mirror of
https://github.com/certd/certd.git
synced 2026-05-18 06:17:31 +08:00
perf: 增加权威NS检查开关,某些用户服务器禁止向黑名单NS服务器发请求
This commit is contained in:
@@ -494,7 +494,7 @@ class AcmeClient {
|
|||||||
throw new Error('Unable to verify ACME challenge, URL not found');
|
throw new Error('Unable to verify ACME challenge, URL not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {challenges} = createChallengeFn({logger:this.logger});
|
const {challenges} = createChallengeFn({logger:this.logger,walkFromAuthoritative: this.opts.walkFromAuthoritative});
|
||||||
|
|
||||||
const verify = challenges
|
const verify = challenges
|
||||||
if (typeof verify[challenge.type] === 'undefined') {
|
if (typeof verify[challenge.type] === 'undefined') {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ async function resolveDomainBySoaRecord(recordName, logger = log) {
|
|||||||
|
|
||||||
async function getAuthoritativeDnsResolver(recordName, logger = log) {
|
async function getAuthoritativeDnsResolver(recordName, logger = log) {
|
||||||
logger(`获取域名${recordName}的权威NS服务器: `);
|
logger(`获取域名${recordName}的权威NS服务器: `);
|
||||||
const resolver = new dns.Resolver({ timeout: 10000,maxTimeout: 60000 });
|
const resolver = new dns.Resolver({timeout: 2000,tries: 2});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Resolve root domain by SOA */
|
/* Resolve root domain by SOA */
|
||||||
@@ -352,3 +352,5 @@ export {
|
|||||||
resolveDomainBySoaRecord
|
resolveDomainBySoaRecord
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,23 @@
|
|||||||
|
|
||||||
import dnsSdk from "dns"
|
import dnsSdk from "dns"
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
import {log as defaultLog} from './logger.js'
|
import { log as defaultLog } from './logger.js'
|
||||||
import axios from './axios.js'
|
import axios from './axios.js'
|
||||||
import * as util from './util.js'
|
import * as util from './util.js'
|
||||||
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
|
import { isAlpnCertificateAuthorizationValid } from './crypto/index.js'
|
||||||
import {utils} from '@certd/basic'
|
import { utils } from '@certd/basic'
|
||||||
|
|
||||||
const dns = dnsSdk.promises
|
const dns = dnsSdk.promises
|
||||||
|
|
||||||
|
let walkFromAuthoritative = true
|
||||||
|
export function setWalkFromAuthoritative(value = true) {
|
||||||
|
walkFromAuthoritative = value
|
||||||
|
}
|
||||||
|
|
||||||
export function createChallengeFn(opts = {}){
|
export function createChallengeFn(opts = {}) {
|
||||||
const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
|
const logger = opts?.logger || { info: defaultLog, error: defaultLog, warn: defaultLog, debug: defaultLog }
|
||||||
|
|
||||||
const log = function(...args){
|
const log = function (...args) {
|
||||||
logger.info(...args)
|
logger.info(...args)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -31,201 +35,210 @@ export function createChallengeFn(opts = {}){
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
|
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
|
||||||
|
|
||||||
async function doQuery(challengeUrl){
|
async function doQuery(challengeUrl) {
|
||||||
log(`正在测试请求 ${challengeUrl} `)
|
log(`正在测试请求 ${challengeUrl} `)
|
||||||
// const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
// const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||||
// const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
|
// const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
|
||||||
|
|
||||||
/* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
|
/* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
|
||||||
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
|
||||||
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
|
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
|
||||||
let data = ""
|
let data = ""
|
||||||
try{
|
try {
|
||||||
const resp = await axios.get(challengeUrl, { httpsAgent });
|
const resp = await axios.get(challengeUrl, { httpsAgent });
|
||||||
data = (resp.data || '').replace(/\s+$/, '');
|
data = (resp.data || '').replace(/\s+$/, '');
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
log(`[error] HTTP request error from ${authz.identifier.value}`,e.message);
|
log(`[error] HTTP request error from ${authz.identifier.value}`, e.message);
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || (data !== keyAuthorization)) {
|
|
||||||
log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
|
||||||
let host = authz.identifier.value;
|
|
||||||
if(utils.domain.isIpv6(host)){
|
|
||||||
host = `[${host}]`;
|
|
||||||
}
|
|
||||||
const challengeUrl = `http://${host}:${httpPort}${suffix}`;
|
|
||||||
|
|
||||||
if (!await doQuery(challengeUrl)) {
|
|
||||||
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
|
||||||
const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
|
|
||||||
const res = await doQuery(httpsChallengeUrl)
|
|
||||||
if (!res) {
|
|
||||||
throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walk DNS until TXT records are found
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
|
||||||
|
|
||||||
let records = [];
|
|
||||||
|
|
||||||
/* Resolve TXT records */
|
|
||||||
try {
|
|
||||||
log(`检查域名 ${recordName} 的TXT记录`);
|
|
||||||
const txtRecords = await resolver.resolveTxt(recordName);
|
|
||||||
if (txtRecords && txtRecords.length) {
|
|
||||||
log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
|
|
||||||
log(`TXT records: ${JSON.stringify(txtRecords)}`);
|
|
||||||
records = records.concat(...txtRecords);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resolve CNAME record first */
|
|
||||||
try {
|
|
||||||
log(`检查是否存在CNAME映射: ${recordName}`);
|
|
||||||
const cnameRecords = await resolver.resolveCname(recordName);
|
|
||||||
|
|
||||||
if (cnameRecords.length) {
|
|
||||||
const cnameRecord = cnameRecords[0];
|
|
||||||
log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
|
|
||||||
let res= await walkTxtRecord(cnameRecord,deep+1);
|
|
||||||
if (res && res.length) {
|
|
||||||
log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
|
|
||||||
records = records.concat(...res);
|
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
log(`没有CNAME映射(${recordName})`);
|
if (!data || (data !== keyAuthorization)) {
|
||||||
|
log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
log(`检查CNAME出错(${recordName}) :${e.message}`);
|
|
||||||
}
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
async function walkTxtRecord(recordName,deep = 0) {
|
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
||||||
if(deep >5){
|
let host = authz.identifier.value;
|
||||||
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
|
if (utils.domain.isIpv6(host)) {
|
||||||
return []
|
host = `[${host}]`;
|
||||||
}
|
}
|
||||||
|
const challengeUrl = `http://${host}:${httpPort}${suffix}`;
|
||||||
|
|
||||||
const txtRecords = []
|
if (!await doQuery(challengeUrl)) {
|
||||||
try {
|
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||||
/* Default DNS resolver first */
|
const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
|
||||||
log('从本地DNS服务器获取TXT解析记录');
|
const res = await doQuery(httpsChallengeUrl)
|
||||||
const res = await walkDnsChallengeRecord(recordName,dns,deep);
|
if (!res) {
|
||||||
if (res && res.length > 0) {
|
throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
|
||||||
for (const item of res) {
|
|
||||||
txtRecords.push(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
log(`本地获取TXT解析记录失败:${e.message}`)
|
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
/**
|
||||||
/* Authoritative DNS resolver */
|
* Walk DNS until TXT records are found
|
||||||
log(`从域名权威服务器获取TXT解析记录`);
|
*/
|
||||||
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName,log);
|
|
||||||
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
|
async function walkDnsChallengeRecord(recordName, resolver = dns, deep = 0) {
|
||||||
if (res && res.length > 0) {
|
|
||||||
for (const item of res) {
|
let records = [];
|
||||||
txtRecords.push(item)
|
|
||||||
|
const isAuthoritative = resolver === dns
|
||||||
|
/* Resolve TXT records */
|
||||||
|
try {
|
||||||
|
log(`检查域名 ${recordName} 的TXT记录(from ${isAuthoritative ? '本地DNS' : '权威DNS服务器'})`);
|
||||||
|
const txtRecords = await resolver.resolveTxt(recordName);
|
||||||
|
if (txtRecords && txtRecords.length) {
|
||||||
|
log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
|
||||||
|
log(`TXT records: ${JSON.stringify(txtRecords)}`);
|
||||||
|
records = records.concat(...txtRecords);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resolve CNAME record first */
|
||||||
|
try {
|
||||||
|
log(`检查是否存在CNAME映射: ${recordName}`);
|
||||||
|
const cnameRecords = await resolver.resolveCname(recordName);
|
||||||
|
|
||||||
|
if (cnameRecords.length) {
|
||||||
|
const cnameRecord = cnameRecords[0];
|
||||||
|
log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
|
||||||
|
let res = await walkTxtRecord(cnameRecord, deep + 1);
|
||||||
|
if (res && res.length) {
|
||||||
|
log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
|
||||||
|
records = records.concat(...res);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`没有CNAME映射(${recordName})`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(`检查CNAME出错(${recordName}) :${e.message}`);
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkTxtRecord(recordName, deep = 0) {
|
||||||
|
if (deep > 5) {
|
||||||
|
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const txtRecords = []
|
||||||
|
try {
|
||||||
|
/* Default DNS resolver first */
|
||||||
|
log('从本地DNS服务器获取TXT解析记录');
|
||||||
|
const res = await walkDnsChallengeRecord(recordName, dns, deep);
|
||||||
|
if (res && res.length > 0) {
|
||||||
|
for (const item of res) {
|
||||||
|
txtRecords.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log(`本地获取TXT解析记录失败:${e.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (walkFromAuthoritative !==false) {
|
||||||
|
try {
|
||||||
|
/* Authoritative DNS resolver */
|
||||||
|
log(`从域名权威服务器获取TXT解析记录`);
|
||||||
|
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName, log);
|
||||||
|
const res = await walkDnsChallengeRecord(recordName, authoritativeResolver, deep);
|
||||||
|
if (res && res.length > 0) {
|
||||||
|
for (const item of res) {
|
||||||
|
txtRecords.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(`权威服务器获取TXT解析记录失败:${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (e) {
|
|
||||||
log(`权威服务器获取TXT解析记录失败:${e.message}`)
|
|
||||||
|
if (txtRecords.length === 0) {
|
||||||
|
throw new Error(`没有找到TXT解析记录(${recordName})`);
|
||||||
|
}
|
||||||
|
return txtRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txtRecords.length === 0) {
|
/**
|
||||||
throw new Error(`没有找到TXT解析记录(${recordName})`);
|
* Verify ACME DNS challenge
|
||||||
}
|
*
|
||||||
return txtRecords;
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
|
||||||
}
|
*
|
||||||
|
* @param {object} authz Identifier authorization
|
||||||
|
* @param {object} challenge Authorization challenge
|
||||||
|
* @param {string} keyAuthorization Challenge key authorization
|
||||||
|
* @param {string} [prefix] DNS prefix
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
|
||||||
* Verify ACME DNS challenge
|
const recordName = `${prefix}${authz.identifier.value}`;
|
||||||
*
|
log(`本地校验TXT记录): ${recordName}`);
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
|
let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
|
||||||
*
|
//去重
|
||||||
* @param {object} authz Identifier authorization
|
recordValues = [...new Set(recordValues)];
|
||||||
* @param {object} challenge Authorization challenge
|
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
||||||
* @param {string} keyAuthorization Challenge key authorization
|
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
||||||
* @param {string} [prefix] DNS prefix
|
const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
|
||||||
* @returns {Promise<boolean>}
|
throw new Error(err);
|
||||||
*/
|
}
|
||||||
|
|
||||||
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
|
log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
|
||||||
const recordName = `${prefix}${authz.identifier.value}`;
|
return true;
|
||||||
log(`本地校验TXT记录): ${recordName}`);
|
|
||||||
let recordValues = await walkTxtRecord(recordName);
|
|
||||||
//去重
|
|
||||||
recordValues = [...new Set(recordValues)];
|
|
||||||
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
|
|
||||||
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
|
||||||
const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
|
/**
|
||||||
return true;
|
* Verify ACME TLS ALPN challenge
|
||||||
}
|
*
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc8737
|
||||||
|
*
|
||||||
|
* @param {object} authz Identifier authorization
|
||||||
|
* @param {object} challenge Authorization challenge
|
||||||
|
* @param {string} keyAuthorization Challenge key authorization
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
||||||
* Verify ACME TLS ALPN challenge
|
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
||||||
*
|
const host = authz.identifier.value;
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8737
|
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
|
||||||
*
|
|
||||||
* @param {object} authz Identifier authorization
|
|
||||||
* @param {object} challenge Authorization challenge
|
|
||||||
* @param {string} keyAuthorization Challenge key authorization
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
|
||||||
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
log('Certificate received from server successfully, matching key authorization in ALPN');
|
||||||
const host = authz.identifier.value;
|
|
||||||
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
|
|
||||||
|
|
||||||
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
|
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
|
||||||
log('Certificate received from server successfully, matching key authorization in ALPN');
|
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
|
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
||||||
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenges:{
|
challenges: {
|
||||||
'http-01': verifyHttpChallenge,
|
'http-01': verifyHttpChallenge,
|
||||||
'dns-01': verifyDnsChallenge,
|
'dns-01': verifyDnsChallenge,
|
||||||
'tls-alpn-01': verifyTlsAlpnChallenge,
|
'tls-alpn-01': verifyTlsAlpnChallenge,
|
||||||
},
|
},
|
||||||
walkTxtRecord,
|
walkTxtRecord,
|
||||||
|
walkDnsChallengeRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
|
||||||
|
|||||||
+3
-1
@@ -219,4 +219,6 @@ export function getAuthoritativeDnsResolver(record:string): Promise<any>;
|
|||||||
|
|
||||||
export const CancelError: typeof CancelError;
|
export const CancelError: typeof CancelError;
|
||||||
|
|
||||||
export function resolveDomainBySoaRecord(domain: string): Promise<string>;
|
export function resolveDomainBySoaRecord(domain: string): Promise<string>;
|
||||||
|
|
||||||
|
export function setWalkFromAuthoritative(value = true): void;
|
||||||
@@ -92,6 +92,9 @@ export class SysPrivateSettings extends BaseSettings {
|
|||||||
environmentVars?: string = '';
|
environmentVars?: string = '';
|
||||||
|
|
||||||
|
|
||||||
|
acmeWalkFromAuthoritative?: boolean = true;
|
||||||
|
|
||||||
|
|
||||||
sms?: {
|
sms?: {
|
||||||
type?: string;
|
type?: string;
|
||||||
config?: any;
|
config?: any;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
|
|||||||
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||||
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
|
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
|
||||||
|
|
||||||
import { getAllSslProviderDomains, setSslProviderReverseProxies } from '@certd/acme-client';
|
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
|
||||||
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
|
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
|
||||||
import { isPlus } from '@certd/plus-core';
|
import { isPlus } from '@certd/plus-core';
|
||||||
import * as dns from 'node:dns';
|
import * as dns from 'node:dns';
|
||||||
@@ -180,6 +180,9 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
|
|
||||||
//加载环境变量
|
//加载环境变量
|
||||||
this.setEnvironmentVars(privateSetting.environmentVars);
|
this.setEnvironmentVars(privateSetting.environmentVars);
|
||||||
|
|
||||||
|
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnvironmentVars(vars: string) {
|
setEnvironmentVars(vars: string) {
|
||||||
|
|||||||
@@ -749,6 +749,18 @@ export default {
|
|||||||
pipelineValidTimeEnabledHelper: "Whether to enable the valid time of the pipeline",
|
pipelineValidTimeEnabledHelper: "Whether to enable the valid time of the pipeline",
|
||||||
certDomainAddToMonitorEnabled: "Add Domain to Certificate Monitor",
|
certDomainAddToMonitorEnabled: "Add Domain to Certificate Monitor",
|
||||||
certDomainAddToMonitorEnabledHelper: "Whether to add the domain to the certificate monitor",
|
certDomainAddToMonitorEnabledHelper: "Whether to add the domain to the certificate monitor",
|
||||||
|
|
||||||
|
defaultCertRenewDays: "Default Certificate Renew Days",
|
||||||
|
defaultCertRenewDaysHelper: "Default certificate renewal days, helpful for table list progress bar display",
|
||||||
|
defaultCertRenewDaysRecommend: "Recommend 15",
|
||||||
|
|
||||||
|
pipelineMaxRunningCount: "Max Running Count",
|
||||||
|
pipelineMaxRunningCountHelper: "Max running count of the pipeline",
|
||||||
|
pipelineMaxRunningCountRecommend: "Recommend 5-15, default 10",
|
||||||
|
|
||||||
|
acmeWalkFromAuthoritative: "Check TXT Record from Authoritative NS",
|
||||||
|
acmeWalkFromAuthoritativeHelper: "Apply certificate when whether to check the TXT record from authoritative NS server first",
|
||||||
|
|
||||||
fixedCertExpireDays: "Fixed Cert Expire Days",
|
fixedCertExpireDays: "Fixed Cert Expire Days",
|
||||||
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
|
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
|
||||||
fixedCertExpireDaysRecommend: "Recommend 90",
|
fixedCertExpireDaysRecommend: "Recommend 90",
|
||||||
|
|||||||
@@ -760,6 +760,8 @@ export default {
|
|||||||
pipelineMaxRunningCount: "同时最大运行流水线数量",
|
pipelineMaxRunningCount: "同时最大运行流水线数量",
|
||||||
pipelineMaxRunningCountHelper: "同一个用户同时运行的最大流水线数量,避免同时触发太多导致ACME账户被限制",
|
pipelineMaxRunningCountHelper: "同一个用户同时运行的最大流水线数量,避免同时触发太多导致ACME账户被限制",
|
||||||
pipelineMaxRunningCountRecommend: "推荐5-15,默认10",
|
pipelineMaxRunningCountRecommend: "推荐5-15,默认10",
|
||||||
|
acmeWalkFromAuthoritative: "从权威NS检查TXT记录",
|
||||||
|
acmeWalkFromAuthoritativeHelper: "申请证书时,是否从权威NS服务器检查TXT记录,如果影响申请证书,可以关闭",
|
||||||
|
|
||||||
fixedCertExpireDays: "固定证书有效期天数",
|
fixedCertExpireDays: "固定证书有效期天数",
|
||||||
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
|
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
|
||||||
@@ -807,6 +809,7 @@ export default {
|
|||||||
environmentVars: "环境变量",
|
environmentVars: "环境变量",
|
||||||
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
|
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
|
||||||
bindUrl: "绑定URL",
|
bindUrl: "绑定URL",
|
||||||
|
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export type SysPrivateSetting = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
config?: any;
|
config?: any;
|
||||||
};
|
};
|
||||||
|
acmeWalkFromAuthoritative?: boolean;
|
||||||
|
|
||||||
//http请求超时时间
|
//http请求超时时间
|
||||||
httpRequestTimeout?: number;
|
httpRequestTimeout?: number;
|
||||||
|
|||||||
@@ -303,8 +303,18 @@ export const useSettingStore = defineStore({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { closable = false } = opts;
|
const { closable = false } = opts;
|
||||||
|
let title = "URL地址未绑定,是否绑定此地址?";
|
||||||
|
let okButtonText = "不,回到原来的地址";
|
||||||
|
let okButtonDanger = false;
|
||||||
|
let forceBack = true;
|
||||||
|
if (closable) {
|
||||||
|
title = "绑定URL";
|
||||||
|
okButtonText = "确定";
|
||||||
|
okButtonDanger = false;
|
||||||
|
forceBack = false;
|
||||||
|
}
|
||||||
const modalRef: any = Modal.warning({
|
const modalRef: any = Modal.warning({
|
||||||
title: "URL地址未绑定,是否绑定此地址?",
|
title: title,
|
||||||
width: 500,
|
width: 500,
|
||||||
keyboard: false,
|
keyboard: false,
|
||||||
closable,
|
closable,
|
||||||
@@ -320,6 +330,7 @@ export const useSettingStore = defineStore({
|
|||||||
绑定到地址1
|
绑定到地址1
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="helper">各类通知里面会以地址1作为URL显示</div>
|
||||||
<div class="flex items-center justify-between mt-3">
|
<div class="flex items-center justify-between mt-3">
|
||||||
<span>
|
<span>
|
||||||
绑定地址2:
|
绑定地址2:
|
||||||
@@ -334,12 +345,14 @@ export const useSettingStore = defineStore({
|
|||||||
},
|
},
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
// await this.doBindUrl();
|
// await this.doBindUrl();
|
||||||
window.location.href = bindUrl;
|
if (forceBack) {
|
||||||
|
window.location.href = bindUrl;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
okButtonProps: {
|
okButtonProps: {
|
||||||
danger: true,
|
danger: okButtonDanger,
|
||||||
},
|
},
|
||||||
okText: "不,回到原来的地址",
|
okText: okButtonText,
|
||||||
// cancelText: "不,回到原来的地址",
|
// cancelText: "不,回到原来的地址",
|
||||||
// onOk: () => {
|
// onOk: () => {
|
||||||
// window.location.href = bindUrl;
|
// window.location.href = bindUrl;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<a-form-item :label="t('certd.sys.setting.bindUrl')">
|
<a-form-item :label="t('certd.sys.setting.bindUrl')">
|
||||||
<a-button class="ml-2" type="primary" @click="settingsStore.openBindUrlModal({ closable: true })">{{ t("certd.sys.setting.bindUrl") }}</a-button>
|
<a-button class="ml-2" type="primary" @click="settingsStore.openBindUrlModal({ closable: true })">{{ t("certd.sys.setting.bindUrl") }}</a-button>
|
||||||
|
<div class="helper" v-html="t('certd.sys.setting.bindUrlHelper')"></div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||||
|
|||||||
@@ -53,6 +53,13 @@
|
|||||||
<div class="helper">{{ t("certd.sys.setting.pipelineMaxRunningCountHelper") }}</div>
|
<div class="helper">{{ t("certd.sys.setting.pipelineMaxRunningCountHelper") }}</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="t('certd.sys.setting.acmeWalkFromAuthoritative')" :name="['private', 'acmeWalkFromAuthoritative']">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a-switch v-model:checked="formState.private.acmeWalkFromAuthoritative" />
|
||||||
|
</div>
|
||||||
|
<div class="helper">{{ t("certd.sys.setting.acmeWalkFromAuthoritativeHelper") }}</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -76,7 +83,9 @@ defineOptions({
|
|||||||
|
|
||||||
const formState = reactive<Partial<SysSettings>>({
|
const formState = reactive<Partial<SysSettings>>({
|
||||||
public: {},
|
public: {},
|
||||||
private: {},
|
private: {
|
||||||
|
acmeWalkFromAuthoritative: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadSysSettings() {
|
async function loadSysSettings() {
|
||||||
|
|||||||
Reference in New Issue
Block a user