| @@ -2,7 +2,7 @@ | |||||
| "apps": [ | "apps": [ | ||||
| { | { | ||||
| "name": "ssl", | "name": "ssl", | ||||
| "script": "www/live.js", | |||||
| "script": "www/production.js", | |||||
| "cwd": "/home/program/front/git/ssl_manage", | "cwd": "/home/program/front/git/ssl_manage", | ||||
| "exec_mode": "cluster", | "exec_mode": "cluster", | ||||
| "instances": 1, | "instances": 1, | ||||
| @@ -15,6 +15,7 @@ | |||||
| "greenlock": "^2.8.9", | "greenlock": "^2.8.9", | ||||
| "greenlock-store-fs": "^3.2.2", | "greenlock-store-fs": "^3.2.2", | ||||
| "le-challenge-fs": "^2.0.9", | "le-challenge-fs": "^2.0.9", | ||||
| "request": "^2.88.2", | |||||
| "scp2": "^0.5.0", | "scp2": "^0.5.0", | ||||
| "source-map-support": "0.4.0", | "source-map-support": "0.4.0", | ||||
| "thinkjs": "v2" | "thinkjs": "v2" | ||||
| @@ -3,15 +3,21 @@ | |||||
| export default { | export default { | ||||
| resource_on: false, | resource_on: false, | ||||
| domains:[ | 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" | CERT_DIR:"/usr/local/nginx/conf/cert" | ||||
| }; | }; | ||||
| @@ -2,13 +2,21 @@ | |||||
| export default { | export default { | ||||
| domains:[ | domains:[ | ||||
| //live | |||||
| "oa.live.educlouddata.com", | "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" | CERT_DIR:"/usr/local/nginx/conf/cert" | ||||
| }; | }; | ||||
| @@ -3,17 +3,21 @@ | |||||
| export default { | export default { | ||||
| resource_on: false, | resource_on: false, | ||||
| domains:[ | domains:[ | ||||
| //live | |||||
| "oa.live.educlouddata.com", | |||||
| //青白江 | |||||
| "oa.qbjjyyun.net", | "oa.qbjjyyun.net", | ||||
| "m.qbjjyyun.net", | |||||
| "ykj.qbjjyyun.net", | |||||
| "m.ykj.qbjjyyun.net", | |||||
| "filea.oa.qbjjyyun.net", | |||||
| "admin.ykj.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", | CERT_DIR:"/etc/nginx/cert", | ||||
| SCP:[ | SCP:[ | ||||
| @@ -1,7 +1,28 @@ | |||||
| 'use strict'; | 'use strict'; | ||||
| import request from 'request'; | |||||
| export default class extends think.controller.base { | 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 }) // 避免自签名证书报错 | agent: new https.Agent({ rejectUnauthorized: false }) // 避免自签名证书报错 | ||||
| }, (res) => { | }, (res) => { | ||||
| const cert = res.socket.getPeerCertificate(); | 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:', '') : ''; | 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); | resolve(cert.valid_to); | ||||
| } else { | } else { | ||||
| reject(new Error('证书信息缺失')); | reject(new Error('证书信息缺失')); | ||||
| @@ -179,26 +181,9 @@ export default class extends Base { | |||||
| } | } | ||||
| } | } | ||||
| if (updateList.length) { | 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 () { | 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); | |||||
| } | |||||
| } | |||||