| @@ -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, | |||
| @@ -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" | |||
| @@ -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" | |||
| }; | |||
| @@ -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" | |||
| }; | |||
| @@ -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:[ | |||
| @@ -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) | |||
| } | |||
| } | |||
| } | |||
| @@ -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 () { | |||
| @@ -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); | |||
| } | |||
| } | |||