核心接口
# applyCertbot 申请接口
// 开始证书的申请 的接口
router.post('/applyCertbot', async (req, res) => {
const { invitationCode, domain } = req.body;
try {
const querySql = `
select nums from ${DB_NAME}.invitation_code_table
where 1=1
and code='${invitationCode}'
`;
const nums = (await runSql(querySql))[0]?.nums;
if (nums > 0) {
const newNums = nums - 1;
const querySql = `
update ${DB_NAME}.invitation_code_table
set nums=${newNums}
where 1=1
and code='${invitationCode}'
`;
await runSql(querySql);
if (!isProd) {
const result = await sendSSHQuery(invitationCode, domain, newNums);
res.send(result);
} else {
const result = await sendQuery(invitationCode, domain, newNums);
res.send(result);
}
} else {
res.send({
error: `邀请码可用次数${nums}`,
});
}
} catch (e) {
console.log('e', e);
res.send({
error: `申请失败 ${e}`,
});
}
});
# downCertbot 下载接口
// 开始证书的下载 接口
router.post('/downCertbot', (req, res) => {
const { processId, domain } = req.body;
const child = globalConn[processId];
if (!child) {
return res.send({
error: '无法找到对应的进程',
});
}
// 继续向进程中发送输入
child.stdin.write('\n'); // 发送回车
let buffer = '';
let responseSent = false;
const sendResponse = (response) => {
if (!responseSent) {
responseSent = true;
res.send(response);
// 确保在发送响应后,正确关闭进程并清理资源
child.kill();
delete globalConn[processId];
}
};
child.stdout.on('data', (data) => {
const output = data.toString();
buffer += output;
console.log('STDOUT:', buffer);
// 正则检测 "verify the TXT record has been deployed" 的提示
const pressEnterRegex = /verify the TXT record has been deployed/i;
if (pressEnterRegex.test(buffer)) {
child.stdin.write('\n'); // 如果匹配到提示,则继续输入回车
}
// 正则检测 "Some challenges have failed" 的提示
const overRegex = /Some challenges have failed/i;
if (overRegex.test(buffer)) {
sendResponse({
error: `证书下载过程中发生错误: ${buffer}`,
});
}
// 正则检测 "Successfully received certificate" 的提示
const successRegex = /Successfully received certificate/i;
const existRegex = /You have an existing certificate/i;
if (successRegex.test(buffer) || existRegex.test(buffer)) {
// 生成压缩包命令
const destinationPath = '/icons/Certificate';
const folderPath = `/etc/letsencrypt/live`;
const zipCommand = `cd ${folderPath} && zip -r ${destinationPath}/${processId}.zip ${domain}`;
exec(zipCommand, (err, stdout, stderr) => {
if (err) {
sendResponse({
error: `执行压缩证书文件夹命令时出错: ${stderr}`,
});
} else {
sendResponse({
success: true,
data: `https://certbot.quantanalysis.cn${destinationPath}/${processId}.zip`,
});
}
});
}
});
child.stderr.on('data', (errData) => {
console.error('STDERR:', errData.toString());
sendResponse({
error: `发生错误: ${errData.toString()}`,
});
});
child.on('close', (code) => {
if (!responseSent) {
sendResponse({
error: '未成功获取到证书下载的结果',
});
}
// 无论如何,确保进程被杀死并清理资源
child.kill();
delete globalConn[processId];
});
});
# 用到的方法
const sendQuery = async (invitationCode, domain, newNums) => {
return new Promise(async (resolve, reject) => {
const processId = Date.now(); // 获取当前时间戳作为 processId
try {
const querySql = `
SELECT process_id, status
FROM ${DB_NAME}.process_id_table
WHERE status='running' AND domain='${domain}'
`;
const results = runSql(querySql);
// 如果有正在进行的进程,则删除该记录
if (results.length > 0) {
await runSql(
`DELETE FROM ${DB_NAME}.process_id_table WHERE domain='${domain}'`,
);
}
const certbotCmd = `sudo certbot certonly --manual --preferred-challenges dns -d "*.${domain}" -d "${domain}"`;
// 初始化 SSH 连接
const child = exec(certbotCmd);
globalConn[processId] = child;
let buffer = '';
let matched = false;
child.stdout.on('data', (data) => {
buffer += data.toString();
console.log('STDOUT:', data.toString());
});
child.stderr.on('data', (errData) => {
console.error('STDERR:', errData.toString());
reject(new Error('STDERR: ' + errData.toString()));
});
// 启动定时器,每秒检查一次缓存的数据
const timer = setInterval(() => {
const regex = /with the following value:\s+([a-zA-Z0-9_-]+)/i;
const match = buffer.match(regex);
if (match && match[1]) {
matched = true;
const txtRecord = match[1];
console.log('提取出的 TXT 记录:', txtRecord);
clearInterval(timer); // 清除定时器
// 保存匹配到的结果到 global.processid
global.processid = global.processid || {};
global.processid[processId] = { txtRecord };
resolve({
success: true,
data: {
text: txtRecord,
newNums,
processId,
},
});
}
const existRegex = /You have an existing certificate/i;
if (existRegex.test(buffer)) {
// 生成压缩包命令
const destinationPath = '/icons/Certificate';
const folderPath = `/etc/letsencrypt/live`;
const zipCommand = `cd ${folderPath} && zip -r ${destinationPath}/${processId}.zip ${domain}`;
exec(zipCommand, (err, stdout, stderr) => {
if (err) {
reject(new Error('error: ' + stderr.toString()));
} else {
resolve({
success: true,
data: {
existUrl: `https://certbot.quantanalysis.cn${destinationPath}/${processId}.zip`,
},
});
}
});
}
}, 1000);
// 10秒后检查是否匹配成功,未成功则终止进程
setTimeout(() => {
if (!matched) {
clearInterval(timer); // 清除定时器
child.kill(); // 终止命令进程
resolve({
error: `10秒内未匹配到所需的 TXT 记录`,
});
}
}, 10000);
} catch (err) {
console.error('错误:', err);
reject(new Error('error: ' + err.toString()));
}
});
};