refactor: remove certd v1 code

This commit is contained in:
xiaojunnuo
2022-10-31 21:27:32 +08:00
parent 6094a4af79
commit e68862b633
113 changed files with 181 additions and 4868 deletions
-14
View File
@@ -1,14 +0,0 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}
-7
View File
@@ -1,7 +0,0 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@certd/certd",
"version": "0.3.0",
"description": "a ssl cert keeper",
"main": "src/index.js",
"scripts": {
"test": "echo \\\"Error: no test specified\\\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"@certd/acme-client": "^0.3.0",
"@certd/api": "^0.3.0",
"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": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}
-202
View File
@@ -1,202 +0,0 @@
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
acme.setLogger((text) => {
logger.info(text)
})
}
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
}
}
-131
View File
@@ -1,131 +0,0 @@
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'
const logger = util.logger
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
@@ -1,127 +0,0 @@
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)
}
}
@@ -1,66 +0,0 @@
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)
}
}
-88
View File
@@ -1,88 +0,0 @@
import chai from 'chai'
import { Certd } from '../src/index.js'
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))
})
})
-14
View File
@@ -1,14 +0,0 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}
-7
View File
@@ -1,7 +0,0 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/
-39
View File
@@ -1,39 +0,0 @@
{
"name": "@certd/executor",
"version": "0.3.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \\\"Error: no test specified\\\" && exit 1",
"build": "webpack --config webpack.config.cjs ",
"rollup": "rollup --config rollup.config.js"
},
"type": "module",
"dependencies": {
"@certd/api": "^0.3.0",
"@certd/certd": "^0.3.0",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20"
},
"devDependencies": {
"@certd/plugin-aliyun": "^0.3.0",
"@certd/plugin-host": "^0.3.0",
"@certd/plugin-tencent": "^0.3.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"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",
"rollup": "^2.35.1",
"rollup-plugin-terser": "^7.0.2"
},
"author": "Greper",
"license": "MIT",
"sideEffects": false,
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}
-21
View File
@@ -1,21 +0,0 @@
import json from '@rollup/plugin-json'
import { terser } from 'rollup-plugin-terser'
import commonjs from '@rollup/plugin-commonjs'
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default {
input: 'src/index.js',
output: [
{
file: 'bundle.js',
format: 'es'
},
{
file: 'bundle.min.js',
format: 'iife',
name: 'version',
plugins: [terser()]
}
],
plugins: [json(), commonjs(), nodeResolve()]
}
-182
View File
@@ -1,182 +0,0 @@
import { Certd } from '@certd/certd'
import { pluginRegistry, util } from '@certd/api'
import _ from 'lodash-es'
import dayjs from 'dayjs'
import { Trace } from './trace.js'
const logger = util.logger
function createDefaultOptions () {
return {
args: {
forceCert: false,
forceDeploy: true,
forceRedeploy: false,
doNotThrowError: false // 部署流程执行有错误时,不抛异常,此时整个任务执行完毕后,可以返回结果,你可以在返回结果中处理
}
}
}
export class Executor {
constructor () {
this.trace = new Trace()
}
async run (options) {
logger.info('------------------- Cert-D ---------------------')
try {
this.transfer(options)
options = _.merge(createDefaultOptions(), options)
return await this.doRun(options)
} catch (e) {
logger.error('任务执行出错', e)
throw e
}
}
transfer (options) {
const providers = options.accessProviders
if (_.isArray(providers)) {
const map = {}
for (const provider of providers) {
if (provider.key) {
map[provider.key] = provider
}
}
options.accessProviders = map
}
}
async doRun (options) {
// 申请证书
logger.info('任务开始')
const certd = new Certd(options)
const cert = await this.runCertd(certd)
if (cert == null) {
throw new Error('申请证书失败')
}
logger.info('证书保存路径:', cert.certDir)
logger.info('----------------------')
if (!cert.isNew) {
// 如果没有更新
if (options.args.forceRedeploy) {
// 强制重新部署,清空保存的状态
await certd.certStore.setCurrentFile('context.json', '{}')
} else if (!options.args.forceDeploy) {
// 且不需要强制deploy
logger.info('证书无更新,无需重新部署')
logger.info('任务完成')
return { cert }
}
}
// 读取上次执行进度
let context = {}
const contextJson = await certd.certStore.getCurrentFile('context.json')
if (contextJson) {
context = JSON.parse(contextJson)
}
context.certIsNew = !!cert.isNew
const trace = new Trace(context)
const resultTrace = trace.getInstance({ type: 'result' })
// 运行部署任务
try {
await this.runDeploys({ options, cert, context, trace })
} finally {
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
}
logger.info('任务完成')
trace.print()
const result = resultTrace.get({ })
if (result) {
if (result.status === 'error' && options.args.doNotThrowError === false) {
throw new Error(result.remark)
}
}
return {
cert,
context,
result
}
}
async runCertd (certd) {
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`)
const cert = await certd.certApply()
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`)
return cert
}
async runDeploys ({ options, cert, context, trace }) {
if (cert == null) {
const certd = new Certd(options)
cert = await certd.readCurrentCert()
}
logger.info('部署任务开始')
for (const deploy of options.deploy) {
const deployName = deploy.deployName
logger.info(`------------【${deployName}】-----------`)
const deployTrace = trace.getInstance({ type: 'deploy', deployName })
if (deploy.disabled === true) {
logger.info('此流程已被禁用,跳过')
logger.info('')
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
deployTrace.set({ tasks: null })
continue
}
try {
for (const task of deploy.tasks) {
if (context[deployName] == null) {
context[deployName] = {}
}
const taskContext = context[deployName]
// 开始执行任务列表
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
}
deployTrace.set({ value: { status: 'success', remark: '执行成功' } })
trace.set({ type: 'result', value: { status: 'success', remark: '执行成功' } })
} catch (e) {
deployTrace.set({ value: { status: 'error', remark: '执行失败:' + e.message } })
trace.set({ type: 'result', value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
logger.error('流程执行失败', e)
}
logger.info('')
}
}
async runTask ({ options, task, cert, context, deploy, trace }) {
const taskType = task.type
const Plugin = pluginRegistry.get(taskType)
const deployName = deploy.deployName
const taskName = task.taskName
if (Plugin == null) {
throw new Error(`插件:${taskType}还未安装`)
}
let instance = Plugin
if (Plugin instanceof Function) {
instance = new Plugin({ accessProviders: options.accessProviders })
}
const taskTrace = trace.getInstance({ type: 'deploy', deployName, taskName })
const traceStatus = taskTrace.get({})
if (traceStatus && traceStatus.status === 'success' && !options.args.forceRedeploy) {
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
taskTrace.set({ value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
return
}
logger.info(`----【${taskName}】开始执行`)
try {
// 执行任务
await instance.execute({ cert, props: task.props, context })
taskTrace.set({ value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
} catch (e) {
taskTrace.set({ value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
throw e
}
logger.info(`----任务【${taskName}】执行完成`)
logger.info('')
}
}
-96
View File
@@ -1,96 +0,0 @@
import { util } from '@certd/api'
import _ from 'lodash-es'
const logger = util.logger
export class Trace {
constructor (context) {
this.context = context
}
getInstance ({ type, deployName, taskName }) {
return {
get: ({ prop }) => {
return this.get({ type, deployName, taskName, prop })
},
set: ({ prop, value }) => {
this.set({ type, deployName, taskName, prop, value })
}
}
}
set ({ type, deployName, taskName, prop, value }) {
const key = this.buildTraceKey({ type, deployName, taskName, prop })
const oldValue = _.get(this.context, key) || {}
_.merge(oldValue, value)
_.set(this.context, key, oldValue)
}
get ({ type, deployName, taskName, prop }) {
return _.get(this.context, this.buildTraceKey({ type, deployName, taskName, prop }))
}
buildTraceKey ({ type = 'default', deployName, taskName, prop }) {
let key = '__trace__.' + type
if (deployName) {
key += '.'
key += deployName.replace(/\./g, '_')
}
if (taskName) {
key += '.tasks.'
key += taskName.replace(/\./g, '_')
}
if (prop) {
key += '.' + prop
}
return key
}
getStringLength (str) {
const enLength = str.replace(/[\u0391-\uFFE5]/g, '').length // 先把中文替换成两个字节的英文,再计算长度
return Math.floor((str.length - enLength) * 1.5) + enLength
}
print () {
const context = this.context
logger.info('---------------------------任务结果总览--------------------------')
if (context.certIsNew) {
this.printTraceLine({ current: 'success', remark: '证书更新成功' }, '更新证书')
} else {
this.printTraceLine({ current: 'skip', remark: '还未到过期时间,跳过' }, '更新证书')
}
const trace = this.get({ type: 'deploy' })
// logger.info('trace', trace)
for (const deployName in trace) {
if (trace[deployName] == null) {
trace[deployName] = {}
}
const traceStatus = this.printTraceLine(trace[deployName], deployName)
const tasks = traceStatus.tasks
if (tasks) {
for (const taskName in tasks) {
if (tasks[taskName] == null) {
tasks[taskName] = {}
}
this.printTraceLine(tasks[taskName], taskName, ' └')
}
}
}
const result = this.get({ type: 'result' })
if (result) {
this.printTraceLine(result, 'result', '')
}
const mainContext = {}
_.merge(mainContext, context)
delete mainContext.__trace__
logger.info('【context】', JSON.stringify(mainContext))
}
printTraceLine (traceStatus, name, prefix = '') {
const length = this.getStringLength(name)
const endPad = _.repeat('-', 45 - prefix.length - length) + '\t'
const status = traceStatus.current || traceStatus.status || ''
const remark = traceStatus.remark || ''
logger.info(`${prefix}${name}${endPad}[${status}] \t${remark}`)
return traceStatus
}
}
-42
View File
@@ -1,42 +0,0 @@
import pkg from 'chai'
import { Executor } from '../src/index.js'
import { createOptions } from '../../../../test/options.js'
import PluginAliyun from '@certd/plugin-aliyun'
import PluginTencent from '@certd/plugin-tencent'
import PluginHost from '@certd/plugin-host'
const { expect } = pkg
// 安装默认插件和授权提供者
PluginAliyun.install()
PluginTencent.install()
PluginHost.install()
describe('AutoDeploy', function () {
it('#run', async function () {
this.timeout(120000)
const options = createOptions()
const executor = new Executor()
const ret = await executor.run(options)
expect(ret).ok
expect(ret.cert).ok
})
it('#forceCert', async function () {
this.timeout(120000)
const executor = new Executor()
const options = createOptions()
options.args.forceCert = true
options.args.forceDeploy = true
const ret = await executor.run(options)
expect(ret).ok
expect(ret.cert).ok
})
it('#forceDeploy', async function () {
this.timeout(120000)
const executor = new Executor()
const options = createOptions()
const ret = await executor.run(options, { forceCert: false, forceDeploy: true, forceRedeploy: true })
expect(ret).ok
expect(ret.cert).ok
})
})
-23
View File
@@ -1,23 +0,0 @@
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
console.log(CleanWebpackPlugin)
module.exports = {
devtool: 'source-map',
target: 'node',
entry: './src/index.js',
output: {
filename: 'executor.js',
path: path.resolve(__dirname, 'dist'),
library: 'certdExecutor',
libraryTarget: 'umd'
},
plugins: [
new CleanWebpackPlugin()
],
mode: 'production'
// mode: 'development',
// optimization: {
// usedExports: true
// }
}
-1
View File
@@ -18,7 +18,6 @@
},
"devDependencies": {
"@types/lodash": "^4.14.186",
"@fast-crud/fast-crud": "^1.5.0",
"vue-tsc": "^0.38.9",
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
+6 -2
View File
@@ -1,10 +1,14 @@
import { Registrable } from "../registry";
import { FormItemProps } from "@fast-crud/fast-crud";
import { accessRegistry } from "./registry";
import { FormItemProps } from "../d.ts";
export type AccessInput = FormItemProps & {
title: string;
required?: boolean;
};
export type AccessDefine = Registrable & {
input: {
[key: string]: FormItemProps;
[key: string]: AccessInput;
};
};
export function IsAccess(define: AccessDefine) {
@@ -7,11 +7,18 @@ import { AbstractAccess } from "../abstract-access";
desc: "",
input: {
accessKeyId: {
title: "accessKeyId",
component: {
placeholder: "accessKeyId",
},
//required: true,
//rules: [{ required: true, message: "必填项" }],
required: true,
},
accessKeySecret: {
title: "accessKeySecret",
component: {
placeholder: "accessKeySecret",
},
required: true,
},
},
})
@@ -0,0 +1,115 @@
/**
* [x]-col的配置
*/
export type ColProps = {
span?: number;
[props: string]: any;
};
export type FormItemProps = {
/**
* 字段label
*/
title?: string;
/**
* 表单字段组件配置
*/
component?: ComponentProps;
/**
* 表单字段 [a|el|n]-col的配置
* 一般用来配置跨列:{span:24} 占满一行
*/
col?: ColProps;
/**
* 默认值
*/
value?: any;
/**
* 帮助提示配置
*/
helper?: string | FormItemHelperProps;
/**
* 排序号
*/
order?: number;
/**
* 是否显示此字段
*/
show?: boolean;
/**
* 是否是空白占位栏
*/
blank?: boolean;
[key: string]: any;
};
/**
* 表单字段帮助说明配置
*/
export type FormItemHelperProps = {
/**
* 自定义渲染帮助说明
* @param scope
*/
render?: (scope: any) => any;
/**
* 帮助文本
*/
text?: string;
/**
* 帮助说明所在的位置,[ undefined | label]
*/
position?: string;
/**
* [a|el|n]-tooltip配置
*/
tooltip?: object;
[key: string]: any;
};
/**
* 组件配置
*/
export type ComponentProps = {
/**
* 组件的名称
*/
name?: string | object;
/**
* vmodel绑定的目标属性名
*/
vModel?: string;
/**
* 当原始组件名的参数被以上属性名占用时,可以配置在这里
* 例如:原始组件有一个叫name的属性,你想要配置它,则可以按如下配置
* ```
* component:{
* name:"组件的名称"
* props:{
* name:"组件的name属性" <-----------
* }
* }
* ```
*/
props?: {
[key: string]: any;
};
/**
* 组件事件监听
*/
on?: {
[key: string]: (context?: any) => void;
};
/**
* 组件其他参数
* 事件:onXxx:(event)=>void 组件原始事件监听
* on.onXxx:(context)=>void 组件事件监听(对原始事件包装)
* 样式:style、class等
*/
[key: string]: any;
};
+1
View File
@@ -1 +1,2 @@
export * from "./pipeline";
export * from "./fast-crud";
+1 -1
View File
@@ -1,6 +1,6 @@
import { FormItemProps } from "@fast-crud/fast-crud";
import { Registrable } from "../registry";
import { pluginRegistry } from "./registry";
import { FormItemProps } from "../d.ts";
export type TaskInput = {
[key: string]: any;
};
@@ -5,7 +5,7 @@ import dayjs from "dayjs";
import { dnsProviderRegistry } from "../../../dns-provider";
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
import { AcmeService } from "./acme";
import _ from "lodash";
export type CertInfo = {
crt: string;
key: string;
@@ -15,6 +15,7 @@ export type CertInfo = {
return {
name: "CertApply",
title: "证书申请",
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
input: {
domains: {
title: "域名",
@@ -22,12 +23,17 @@ export type CertInfo = {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
},
required: true,
col: {
span: 24,
},
helper: "请输入域名",
helper:
"支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work\n" +
"支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com\n" +
"输入一个回车之后,再输入下一个",
},
email: {
title: "邮箱",
@@ -138,7 +144,17 @@ export class CertApplyPlugin extends AbstractPlugin implements TaskPlugin {
const domains = input["domains"];
const dnsProviderType = input["dnsProviderType"];
const dnsProviderAccessId = input["dnsProviderAccess"];
const csrInfo = input["csrInfo"];
const csrInfo = _.merge(
{
country: "CN",
state: "GuangDong",
locality: "ShengZhen",
organization: "CertD Org.",
organizationUnit: "IT Department",
emailAddress: email,
},
input["csrInfo"]
);
this.logger.info("开始申请证书,", email, domains);
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
@@ -59,11 +59,21 @@ export class Registry<T extends typeof AbstractRegistrable> {
getDefineList() {
const list = [];
for (const key in this.storage) {
const PluginClass = this.storage[key];
// @ts-ignore
const plugin = new PluginClass();
list.push({ ...plugin.define, key });
const define = this.getDefine(key);
if (define) {
list.push({ ...define, key });
}
}
return list;
}
getDefine(key: string) {
const PluginClass = this.storage[key];
if (!PluginClass) {
return;
}
// @ts-ignore
const plugin = new PluginClass();
return plugin.define;
}
}
@@ -9,6 +9,7 @@ export const pipeline: Pipeline = {
version: 1,
id: generateId(),
title: "测试管道",
userId: 1,
triggers: [],
stages: [
{
+13
View File
@@ -7,5 +7,18 @@ export default defineConfig({
entry: "src/index.ts",
name: "pipeline",
},
rollupOptions: {
external: ["vue", "lodash-es", "dayjs", "@fast-crud/fast-crud"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: "Vue",
"lodash-es": "_",
dayjs: "dayjs",
"@fast-crud/fast-crud": "FastCrud",
},
},
},
},
});