核心接口

# 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()));
    }
  });
};