refactor: dnspod支持

master
xiaojunnuo 2020-12-24 00:49:31 +08:00
parent 49b96592a0
commit 71f6d5769a
13 changed files with 300 additions and 95 deletions

View File

@ -12,9 +12,12 @@
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@types/node": "^14.14.13",
"axios": "^0.21.1",
"dayjs": "^1.9.7",
"lodash": "^4.17.20",
"log4js": "^6.3.0",
"node-forge": "^0.10.0",
"qs": "^6.9.4",
"@certd/acme-client": "^0.0.1"
},
"devDependencies": {

View File

@ -1,47 +1,58 @@
import log from './utils/util.log.js'
import acme from '@certd/acme-client'
import _ from 'lodash'
import path from 'path'
import sleep from './utils/util.sleep.js'
export class AcmeService {
constructor (store) {
this.store = store
}
async getAccountKey (email) {
let key = this.store.get(this.buildAccountKeyPath(email))
if (key == null) {
key = await this.createNewKey({ email })
async getAccountConfig (email) {
let conf = this.store.get(this.buildAccountPath(email))
if (conf == null) {
conf = {}
} else {
conf = JSON.parse(conf)
}
return key
return conf
}
buildAccountKeyPath (email) {
return email + '/acme/account.key'
buildAccountPath (email) {
return path.join(email, '/account.json')
}
setAccountKey (email, privateKey) {
this.store.set(this.buildAccountKeyPath(email), privateKey)
saveAccountConfig (email, conf) {
this.store.set(this.buildAccountPath(email), JSON.stringify(conf))
}
async getAcmeClient (email) {
const key = await this.getAccountKey(email)
const conf = await this.getAccountConfig(email)
if (conf.key == null) {
conf.key = await this.createNewKey()
this.saveAccountConfig(email, conf)
}
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: key,
backoffAttempts: 10,
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 ({ email }) {
const privateKey = await acme.forge.createPrivateKey()
this.setAccountKey(email, privateKey)
}
async loggerin () {
async createNewKey () {
const key = await acme.forge.createPrivateKey()
return key.toString()
}
async challengeCreateFn (authz, challenge, keyAuthorization, dnsProvider) {
@ -67,7 +78,11 @@ export class AcmeService {
/* Replace this */
log.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`)
return await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue)
return await dnsProvider.createRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: recordValue
})
}
}
@ -77,10 +92,12 @@ export class AcmeService {
* @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, dnsProvider) {
async challengeRemoveFn (authz, challenge, keyAuthorization, recordItem, dnsProvider) {
log.info('Triggered challengeRemoveFn()')
/* http-01 */
@ -100,12 +117,24 @@ export class AcmeService {
/* Replace this */
log.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`)
await dnsProvider.removeRecord(dnsRecord, 'TXT', keyAuthorization)
await dnsProvider.removeRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: keyAuthorization,
record: recordItem
})
}
}
async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo }) {
const client = await this.getAcmeClient(email)
let accountUrl
try {
accountUrl = client.getAccountUrl()
} catch (e) {
}
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains)
@ -120,7 +149,7 @@ export class AcmeService {
if (dnsProvider == null) {
throw new Error('dnsProvider 不能为空')
}
/* Certificate */
/* 自动申请证书 */
const crt = await client.auto({
csr,
email: email,
@ -129,11 +158,20 @@ export class AcmeService {
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider)
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
return await this.challengeRemoveFn(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) {
log.warn('保存accountUrl出错', e)
}
}
/* Done */
log.debug(`CSR:\n${csr.toString()}`)
log.debug(`Certificate:\n${crt.toString()}`)

View File

@ -1,9 +1,29 @@
import _ from 'lodash'
export class DnsProvider {
createRecord (dnsRecord, type, recordValue) {
async createRecord ({ fullRecord, type, value }) {
throw new Error('请实现 createRecord 方法')
}
removeRecord (dnsRecord, type, recordValue) {
async removeRecord ({ fullRecord, type, value, record }) {
throw new Error('请实现 removeRecord 方法')
}
async getDomainList () {
throw new Error('请实现 getDomainList 方法')
}
async matchDomain (dnsRecord, domainPropName) {
const list = await this.getDomainList()
let domain = null
for (const item of list) {
if (_.endsWith(dnsRecord, item[domainPropName])) {
domain = item
break
}
}
if (!domain) {
throw new Error('找不到域名,请检查域名是否正确:' + dnsRecord)
}
return domain
}
}

View File

@ -59,17 +59,18 @@ export default class AliyunDnsProvider extends DnsProvider {
return ret.DomainRecords.Record
}
async createRecord (dnsRecord, type, recordValue) {
log.info('添加域名解析:', dnsRecord, recordValue)
const domain = await this.matchDomain(dnsRecord)
const rr = dnsRecord.replace('.' + domain, '')
async createRecord ({ fullRecord, type, value }) {
log.info('添加域名解析:', fullRecord, value)
const domain = await this.matchDomain(fullRecord)
const rr = fullRecord.replace('.' + domain, '')
const params = {
RegionId: 'cn-hangzhou',
DomainName: domain,
RR: rr,
Type: type,
Value: recordValue
Value: value,
Line: 'oversea' // 海外
}
const requestOption = {
@ -78,7 +79,7 @@ export default class AliyunDnsProvider extends DnsProvider {
try {
const ret = await this.client.request('AddDomainRecord', params, requestOption)
console.log('添加域名解析成功:', dnsRecord, recordValue, ret.RecordId)
console.log('添加域名解析成功:', value, value, ret.RecordId)
return ret.RecordId
} catch (e) {
// e.code === 'DomainRecordDuplicate'
@ -87,15 +88,10 @@ export default class AliyunDnsProvider extends DnsProvider {
}
}
async removeRecord (dnsRecord, type, value) {
const domain = await this.matchDomain(dnsRecord)
const rr = dnsRecord.replace('.' + domain, '')
const record = await this.getRecords(domain, rr, value)
async removeRecord ({ fullRecord, type, value, record }) {
const params = {
RegionId: 'cn-hangzhou',
RecordId: record[0].RecordId
RecordId: record
}
const requestOption = {
@ -103,7 +99,7 @@ export default class AliyunDnsProvider extends DnsProvider {
}
const ret = await this.client.request('DeleteDomainRecord', params, requestOption)
log.info('删除域名解析成功:', dnsRecord, value, ret.RecordId)
log.info('删除域名解析成功:', fullRecord, value, ret.RecordId)
return ret.RecordId
}
}

View File

@ -0,0 +1,76 @@
import { DnsProvider } from '../dns-provider.js'
import _ from 'lodash'
import log from '../../utils/util.log.js'
import { request } from '../../utils/util.request.js'
export default class DnspodDnsProvider extends DnsProvider {
constructor (dnsProviderConfig) {
super()
if (!dnsProviderConfig.id || !dnsProviderConfig.token) {
throw new Error('请正确配置dnspod的 id 和 token')
}
this.loginToken = dnsProviderConfig.id + ',' + dnsProviderConfig.token
}
async doRequest (options) {
const config = {
method: 'post',
formData: {
login_token: this.loginToken,
format: 'json',
lang: 'cn',
error_on_empty: 'no'
},
timeout: 5000
}
_.merge(config, options)
const ret = await request(config)
if (ret?.status?.code !== '1') {
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
}
return ret
}
async getDomainList () {
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Domain.List'
})
log.debug('dnspod 域名列表:', ret.domains)
return ret.domains
}
async createRecord ({ fullRecord, type, value }) {
log.info('添加域名解析:', fullRecord, value)
const domainItem = await this.matchDomain(fullRecord, 'name')
const domain = domainItem.name
const rr = fullRecord.replace('.' + domain, '')
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Record.Create',
formData: {
domain,
sub_domain: rr,
record_type: type,
record_line: '默认',
value: value,
mx: 1
}
})
console.log('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
return ret.record
}
async removeRecord ({ fullRecord, type, value, record }) {
const domain = await this.matchDomain(fullRecord, 'name')
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Record.Remove',
formData: {
domain,
record_id: record.id
}
})
log.info('删除域名解析成功:', fullRecord, value)
return ret.RecordId
}
}

View File

@ -95,7 +95,7 @@ export class Certd {
async createDnsProvider (options) {
const accessProviders = options.accessProviders
const providerOptions = accessProviders[options.cert.challenge.dnsProvider]
const providerOptions = accessProviders[options.cert.dnsProvider]
return await DnsProviderFactory.createByType(providerOptions.providerType, providerOptions)
}

View File

@ -0,0 +1,56 @@
import axios from 'axios'
import log from './util.log.js'
import qs from 'qs'
/**
* @description 创建请求实例
*/
function createService () {
// 创建一个 axios 实例
const service = axios.create()
// 请求拦截
service.interceptors.request.use(
config => {
if (config.formData) {
config.data = qs.stringify(config.formData, {
arrayFormat: 'indices',
allowDots: true
}) // 序列化请求参数
delete config.formData
}
return config
},
error => {
// 发送失败
log.error(error)
return Promise.reject(error)
}
)
// 响应拦截
service.interceptors.response.use(
response => {
return response.data
},
error => {
// const status = _.get(error, 'response.status')
// switch (status) {
// case 400: error.message = '请求错误'; break
// case 401: error.message = '未授权,请登录'; break
// case 403: error.message = '拒绝访问'; break
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
// case 408: error.message = '请求超时'; break
// case 500: error.message = '服务器内部错误'; break
// case 501: error.message = '服务未实现'; break
// case 502: error.message = '网关错误'; break
// case 503: error.message = '服务不可用'; break
// case 504: error.message = '网关超时'; break
// case 505: error.message = 'HTTP版本不受支持'; break
// default: break
// }
log.error('请求出错:', error.response.config.url, error)
return Promise.reject(error)
}
)
return service
}
export const request = createService()

View File

@ -1,6 +1,7 @@
import pkg from 'chai'
import options from '../options.js'
import AliyunDnsProvider from '../../src/dns-provider/impl/aliyun.js'
import { Certd } from '../../src/index.js'
const { expect } = pkg
describe('AliyunDnsProvider', function () {
it('#getDomainList', async function () {
@ -17,17 +18,26 @@ describe('AliyunDnsProvider', function () {
expect(recordList.length).gt(0)
})
it('#createRecord', async function () {
it('#createAndRemoveRecord', async function () {
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
const recordId = await aliyunDnsProvider.createRecord('___certd___.__test__.docmirror.cn', 'TXT', 'aaaa')
const record = await aliyunDnsProvider.createRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa' })
console.log('recordId', record)
expect(record != null).ok
const recordId = await aliyunDnsProvider.removeRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa', record })
console.log('recordId', recordId)
expect(recordId != null).ok
})
it('#removeRecord', async function () {
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
const recordId = await aliyunDnsProvider.removeRecord('___certd___.__test__.docmirror.cn', 'TXT', 'aaaa')
console.log('recordId', recordId)
expect(recordId != null).ok
it('#申请证书-aliyun', async function () {
this.timeout(300000)
options.args = { forceCert: true }
const certd = new Certd()
const cert = await certd.certApply(options)
expect(cert).ok
expect(cert.crt).ok
expect(cert.key).ok
expect(cert.detail).ok
expect(cert.expires).ok
})
})

View File

@ -0,0 +1,36 @@
import pkg from 'chai'
import options from '../options.js'
import DnspodDnsProvider from '../../src/dns-provider/impl/dnspod.js'
import { Certd } from '../../src/index.js'
const { expect } = pkg
describe('DnspodDnsProvider', function () {
it('#getDomainList', async function () {
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
const domainList = await dnsProvider.getDomainList()
console.log('domainList', domainList)
expect(domainList.length).gt(0)
})
it('#createRecord&removeRecord', async function () {
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
const record = await dnsProvider.createRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa' })
console.log('recordId', record.id)
expect(record.id != null).ok
await dnsProvider.removeRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa', record })
})
it('#申请证书', async function () {
this.timeout(300000)
options.cert.domains = ['*.certd.xyz', 'certd.xyz']
options.cert.dnsProvider = 'dnspod'
options.args = { forceCert: true }
const certd = new Certd()
const cert = await certd.certApply(options)
expect(cert).ok
expect(cert.crt).ok
expect(cert.key).ok
expect(cert.detail).ok
expect(cert.expires).ok
})
})

View File

@ -14,20 +14,9 @@ describe('Certd', function () {
const certd = new Certd()
certd.writeCert('xiaojunnuo@qq.com', ['*.domain.cn'], { csr: 'csr', crt: 'aaa', key: 'bbb' })
})
it('#申请证书-aliyun', async function () {
this.timeout(300000)
options.args = { forceCert: true }
const certd = new Certd()
const cert = await certd.certApply(options)
expect(cert).ok
expect(cert.crt).ok
expect(cert.key).ok
expect(cert.detail).ok
expect(cert.expires).ok
})
it('#readCurrentCert', async function () {
const certd = new Certd()
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.domain.cn'])
expect(cert).to.be.ok
expect(cert.crt).ok
expect(cert.key).to.be.ok

View File

@ -19,10 +19,7 @@ const defaultOptions = {
cert: {
domains: ['*.docmirror.club', 'docmirror.club'],
email: 'xiaojunnuo@qq.com',
challenge: {
challengeType: 'dns',
dnsProvider: 'aliyun'
},
dnsProvider: 'aliyun',
csrInfo: {
country: 'CN',
state: 'GuangDong',

View File

@ -65,17 +65,6 @@
resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
acme-client@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/acme-client/-/acme-client-4.1.2.tgz#6072e98cc2e5aaf0a3f769a823b6bc68d034e8da"
integrity sha512-3GlqDVWHgm0xpfnwOME/OpEBwEgO2vOplGEN8miWS7n7A28U9C7MtuTg6AuPYo8Lmqu4SADllnZrMLNasVNLEQ==
dependencies:
axios "0.21.0"
backo2 "^1.0.0"
bluebird "^3.5.0"
debug "^4.1.1"
node-forge "^0.10.0"
acorn-jsx@^5.3.1:
version "5.3.1"
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
@ -175,18 +164,13 @@ astral-regex@^1.0.0:
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
axios@0.21.0:
version "0.21.0"
resolved "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
backo2@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -202,11 +186,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
bluebird@^3.5.0:
version "3.7.2"
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -750,9 +729,9 @@ flatted@^3.1.0:
integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
version "1.13.1"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
fs-extra@^8.1.0:
version "8.1.0"
@ -1404,6 +1383,11 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@^6.9.4:
version "6.9.4"
resolved "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"

@ -1 +1 @@
Subproject commit 5a3f7446222fa123b645e51e954ccfd981f459bd
Subproject commit 334a73743d03ca401258dce7ed479a969383689e