diff --git a/devops/pm2.json b/devops/pm2.json index 6749098..1d7ca1b 100644 --- a/devops/pm2.json +++ b/devops/pm2.json @@ -2,7 +2,7 @@ "apps": [ { "name": "ssl", - "script": "www/live.js", + "script": "www/production.js", "cwd": "/home/program/front/git/ssl_manage", "exec_mode": "cluster", "instances": 1, diff --git a/package.json b/package.json index cda4f82..d636629 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "greenlock": "^2.8.9", "greenlock-store-fs": "^3.2.2", "le-challenge-fs": "^2.0.9", + "request": "^2.88.2", "scp2": "^0.5.0", "source-map-support": "0.4.0", "thinkjs": "v2" diff --git a/src/common/config/env/development.js b/src/common/config/env/development.js index a7cab58..8717ac5 100644 --- a/src/common/config/env/development.js +++ b/src/common/config/env/development.js @@ -3,15 +3,21 @@ export default { resource_on: false, domains:[ - // "oa.live.educlouddata.com", - // "m.live.educlouddata.com", - // "yun.live.educlouddata.com", - // "yunh5.live.educlouddata.com", - // "kms.live.educlouddata.com", - // "ams.live.educlouddata.com", - // "data.live.educlouddata.com", - "live.qqsrx.top", - // "www.qbjjyyun.net", + //live + "oa.live.educlouddata.com", + //青白江 + "oa.qbjjyyun.net", + "filea.oa.qbjjyyun.net", + "admin.ykj.qbjjyyun.net", + "m.ykj.qbjjyyun.net", + //青白江官网 + "qbjjy.cn", + //金苹果 + "oa.61gx.com", + //数字校园 + "oa.educlouddata.com", + //南坪中学 + "oa.npzx.org.cn", ], CERT_DIR:"/usr/local/nginx/conf/cert" }; \ No newline at end of file diff --git a/src/common/config/env/live.js b/src/common/config/env/live.js index bb8066f..7aedab2 100644 --- a/src/common/config/env/live.js +++ b/src/common/config/env/live.js @@ -2,13 +2,21 @@ export default { domains:[ + //live "oa.live.educlouddata.com", - "m.live.educlouddata.com", - "yun.live.educlouddata.com", - "yunh5.live.educlouddata.com", - "kms.live.educlouddata.com", - "ams.live.educlouddata.com", - "data.live.educlouddata.com", + //青白江 + "oa.qbjjyyun.net", + "filea.oa.qbjjyyun.net", + "admin.ykj.qbjjyyun.net", + "m.ykj.qbjjyyun.net", + //青白江官网 + "qbjjy.cn", + //金苹果 + "oa.61gx.com", + //数字校园 + "oa.educlouddata.com", + //南坪中学 + "oa.npzx.org.cn", ], CERT_DIR:"/usr/local/nginx/conf/cert" }; \ No newline at end of file diff --git a/src/common/config/env/production.js b/src/common/config/env/production.js index dd76acd..7b70813 100644 --- a/src/common/config/env/production.js +++ b/src/common/config/env/production.js @@ -3,17 +3,21 @@ export default { resource_on: false, domains:[ + //live + "oa.live.educlouddata.com", + //青白江 "oa.qbjjyyun.net", - "m.qbjjyyun.net", - "ykj.qbjjyyun.net", - "m.ykj.qbjjyyun.net", + "filea.oa.qbjjyyun.net", "admin.ykj.qbjjyyun.net", - "kms.qbjjyyun.net", - "ams.qbjjyyun.net", - "data.qbjjyyun.net", - "xxzz.qbjjyyun.net", - "xxzz.h5.qbjjyyun.net", - "www.qbjjyyun.net", + "m.ykj.qbjjyyun.net", + //青白江官网 + "qbjjy.cn", + //金苹果 + "oa.61gx.com", + //数字校园 + "oa.educlouddata.com", + //南坪中学 + "oa.npzx.org.cn", ], CERT_DIR:"/etc/nginx/cert", SCP:[ diff --git a/src/home/controller/base.js b/src/home/controller/base.js index 1c45bd9..b304a24 100644 --- a/src/home/controller/base.js +++ b/src/home/controller/base.js @@ -1,7 +1,28 @@ 'use strict'; - +import request from 'request'; export default class extends think.controller.base { + /** - * some base method in here + * 监控 + * @param {*} msg */ + async monitor (msg) { + try { + let logStr = `探测域名证书:\n`; + let temp = "=".repeat(30); + logStr += `${temp}\n`; + logStr += `${msg}\n`; + logStr += `${temp}\n`; + let requestData = { + msgtype: "text", + text: { + "content": `${logStr}` + } + }; + const PUSH_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=8f2158c7-953b-47d5-be2c-79c9fd228533"; + request({ url: PUSH_URL, method: 'POST', json: true, headers: { "content-type": "application/json", }, body: requestData }); + } catch (e) { + console.log(e) + } + } } \ No newline at end of file diff --git a/src/home/controller/index.js b/src/home/controller/index.js index dbe8069..9416113 100644 --- a/src/home/controller/index.js +++ b/src/home/controller/index.js @@ -91,9 +91,11 @@ function checkCertExpiry (domain) { agent: new https.Agent({ rejectUnauthorized: false }) // 避免自签名证书报错 }, (res) => { const cert = res.socket.getPeerCertificate(); - // console.log(cert); + console.log("=".repeat(20), `${domain}证书信息`, "=".repeat(20)) + console.log(cert); + console.log("=".repeat(20), `${domain}证书end`, "=".repeat(20)) let subjectaltname = cert.subjectaltname ? cert.subjectaltname.replace('DNS:', '') : ''; - if (cert.valid_to && subjectaltname == domain) { + if (cert.valid_to && subjectaltname.includes(domain)) { resolve(cert.valid_to); } else { reject(new Error('证书信息缺失')); @@ -179,26 +181,9 @@ export default class extends Base { } } if (updateList.length) { - console.log("==".repeat(20), "开始生成证书", "==".repeat(20)); - console.log('开始时间:', dayjs().format('YYYY-MM-DD HH:mm:ss')); - for (let domain of updateList) { - try { - let email = "waitshan@163.com" - const results = await CreateSSL(domain, email); - console.log(`证书${domain},生成成功`); - } catch (e) { - console.log("==".repeat(20), "证书生成失败", "==".repeat(20)); - console.log("域名:", domain); - console.log(e); - console.log("==".repeat(20), "证书生成失败", "==".repeat(20)); - } - } - await this.deployCert(updateList); - console.log(`更新证书数量:${updateList.length}`); - console.log('结束时间:', dayjs().format('YYYY-MM-DD HH:mm:ss')); - console.log("==".repeat(20), "结束生成证书", "==".repeat(20)); + } - this.success(); + this.success(updateList); } async createAction () { diff --git a/src/home/controller/ssl.js b/src/home/controller/ssl.js new file mode 100644 index 0000000..2fa0563 --- /dev/null +++ b/src/home/controller/ssl.js @@ -0,0 +1,139 @@ +'use strict'; + +import Base from './base.js'; + +const https = require('https'); +const dayjs = require('dayjs'); + +/** + * 检查域名是否匹配证书的 subjectaltname(支持通配符) + * @param {string} domain - 要检查的域名 + * @param {string} subjectaltname - 证书的 subjectaltname 字段 + * @returns {boolean} + */ +function matchesCertDomain (domain, subjectaltname) { + if (!subjectaltname) return false; + + // 提取所有域名(去除 DNS: 前缀) + const certDomains = subjectaltname.split(',').map(d => d.replace(/DNS:/gi, '').trim()); + + for (let certDomain of certDomains) { + // 直接匹配 + if (certDomain === domain) { + return true; + } + + // 通配符匹配(如 *.example.com) + if (certDomain.startsWith('*.')) { + const baseDomain = certDomain.slice(2); // 去掉 '*.' + // 检查域名是否以该基础域名结尾,且前面有子域名 + if (domain.endsWith('.' + baseDomain)) { + return true; + } + } + } + + return false; +} + +/** + * 检查证书过期时间 + * @param {*} domain + * @returns + */ +function checkCertExpiry (domain) { + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: domain, + port: 443, + method: 'GET', + timeout: 10000, // 10秒超时 + family: 4, // 强制使用 IPv4 + agent: new https.Agent({ + rejectUnauthorized: false, // 避免自签名证书报错 + timeout: 10000 + }) + }, (res) => { + const cert = res.socket.getPeerCertificate(); + console.log("=".repeat(20), `${domain}证书信息`, "=".repeat(20)) + console.log(cert); + console.log("=".repeat(20), `${domain}证书end`, "=".repeat(20)) + + const subjectaltname = cert.subjectaltname || ''; + + if (cert.valid_to && matchesCertDomain(domain, subjectaltname)) { + resolve(cert.valid_to); + } else { + reject(new Error(`证书信息缺失或域名不匹配。域名: ${domain}, SAN: ${subjectaltname}`)); + } + }); + + req.on('error', (err) => reject(err)); + req.end(); + }); +} + +/** + * + * @param {*} domain + * @returns + */ +async function getRemainingDays (domain) { + try { + const validTo = await checkCertExpiry(domain); + const expiryDate = dayjs(validTo, 'MMM DD HH:mm:ss YYYY GMT'); // 解析时间格式 + const currentDate = dayjs(); + const daysLeft = expiryDate.diff(currentDate, 'day'); + return { + domain, + daysLeft + }; + } catch (err) { + console.error(`检测失败:${err.message}`); + return { + domain, + daysLeft: -1 + }; // 错误时返回-1 + } +} + + +export default class extends Base { + /** + * index action + * @return {Promise} [] + */ + async indexAction () { + return this.display(); + } + + /** + * 检查并更新证书 + */ + async checksslAction () { + let domains = this.config().domains; + let updateList = []; //需要更新的数据 + for (let domain of domains) { + try { + const { daysLeft } = await getRemainingDays(domain); + const msg = `证书${domain},剩余${daysLeft}天`; + console.log(msg); + if (daysLeft <= 10) { + if(daysLeft <= 0){ + updateList.push(`${domain} 已过期:${Math.abs(daysLeft)}天`); + }else{ + updateList.push(`${domain} 剩余时间:${daysLeft}天`); + } + } + } catch (e) { + console.log("==".repeat(20)) + console.log('检查证书失败:', e); + console.log("==".repeat(20)) + } + } + if (updateList.length) { + this.monitor(updateList.join('\n')); + } + this.success(updateList); + } +} \ No newline at end of file