|
|
|
@@ -0,0 +1,236 @@ |
|
|
|
'use strict'; |
|
|
|
|
|
|
|
import Base from './base.js'; |
|
|
|
|
|
|
|
const Greenlock = require('greenlock'); |
|
|
|
const path = require('path'); |
|
|
|
const GreenlockStoreFs = require('greenlock-store-fs'); |
|
|
|
const LeChallengeFs = require('le-challenge-fs'); |
|
|
|
const fs = require('fs-extra'); |
|
|
|
const { execSync } = require('child_process'); |
|
|
|
const https = require('https'); |
|
|
|
const dayjs = require('dayjs'); // 需安装dayjs库 |
|
|
|
|
|
|
|
|
|
|
|
// SSL根目录 |
|
|
|
const ROOT_PATH = path.join(think.ROOT_PATH, 'ssl'); |
|
|
|
const PEM_PATH = path.join(ROOT_PATH, 'cert'); |
|
|
|
// 创建存储对象 |
|
|
|
const leStore = GreenlockStoreFs.create({ |
|
|
|
configDir: path.join(ROOT_PATH, 'letsencrypt'), |
|
|
|
}); |
|
|
|
// 创建验证对象 |
|
|
|
const leHttpChallenge = LeChallengeFs.create({ |
|
|
|
webrootPath: path.join(ROOT_PATH, 'lechallenge'), |
|
|
|
}); |
|
|
|
// 是否同意协议 |
|
|
|
function leAgree (opts, agreeCb) { |
|
|
|
agreeCb(null, opts.tosUrl); |
|
|
|
} |
|
|
|
|
|
|
|
// 证书申请对象 |
|
|
|
const greenlock = Greenlock.create({ |
|
|
|
version: 'draft-12', |
|
|
|
// 测试环境 |
|
|
|
server: 'https://acme-staging-v02.api.letsencrypt.org/directory', |
|
|
|
// 生产环境 |
|
|
|
// server: 'https://acme-v02.api.letsencrypt.org/directory', |
|
|
|
store: leStore, |
|
|
|
challenges: { |
|
|
|
'http-01': leHttpChallenge, |
|
|
|
}, |
|
|
|
challengeType: 'http-01', |
|
|
|
agreeToTerms: leAgree, |
|
|
|
debug: true, |
|
|
|
renewBy: 10 * 24 * 60 * 60 * 1000,// 10倒计时开始续期 |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 创建SSL证书申请请求 |
|
|
|
async function CreateSSL (domain, email = '') { |
|
|
|
const results = await greenlock.register({ |
|
|
|
domains: [domain], |
|
|
|
email, |
|
|
|
agreeTos: true, |
|
|
|
rsaKeySize: 2048, |
|
|
|
}); |
|
|
|
// 如果生成证书就保存一次证书 |
|
|
|
const dir = path.join(PEM_PATH, domain); |
|
|
|
if (!fs.existsSync(dir)) { |
|
|
|
fs.mkdirSync(dir); |
|
|
|
} |
|
|
|
if (results.cert && results.chain) { |
|
|
|
fs.writeFileSync(path.join(dir, `${domain}.crt`), results.cert + results.chain, 'utf-8'); |
|
|
|
console.log(`证书${domain}.crt创建成功!`); |
|
|
|
} |
|
|
|
if (results.privkey) { |
|
|
|
fs.writeFileSync(path.join(dir, `${domain}.key`), results.privkey, 'utf-8'); |
|
|
|
console.log(`证书${domain}.key创建成功!`); |
|
|
|
} |
|
|
|
return results; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 检查证书过期时间 |
|
|
|
* @param {*} domain |
|
|
|
* @returns |
|
|
|
*/ |
|
|
|
function checkCertExpiry (domain) { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
const req = https.request({ |
|
|
|
hostname: domain, |
|
|
|
port: 443, |
|
|
|
method: 'GET', |
|
|
|
agent: new https.Agent({ rejectUnauthorized: false }) // 避免自签名证书报错 |
|
|
|
}, (res) => { |
|
|
|
const cert = res.socket.getPeerCertificate(); |
|
|
|
if (cert.valid_to) { |
|
|
|
resolve(cert.valid_to); |
|
|
|
} else { |
|
|
|
reject(new Error('证书信息缺失')); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
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 daysLeft; |
|
|
|
} catch (err) { |
|
|
|
console.error(`检测失败:${err.message}`); |
|
|
|
return -1; // 错误时返回-1 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default class extends Base { |
|
|
|
/** |
|
|
|
* index action |
|
|
|
* @return {Promise} [] |
|
|
|
*/ |
|
|
|
async indexAction () { |
|
|
|
let domains = this.config().domains; |
|
|
|
let msgs = [] |
|
|
|
for (let domain of domains) { |
|
|
|
try { |
|
|
|
const daysLeft = await getRemainingDays(domain); |
|
|
|
const msg = `证书${domain},剩余${daysLeft}天` |
|
|
|
console.log(msg); |
|
|
|
msgs.push(msg); |
|
|
|
} catch (e) { |
|
|
|
const msg = `证书${domain},证书缺失或无法访问` |
|
|
|
console.log(e); |
|
|
|
msgs.push(msg); |
|
|
|
} |
|
|
|
} |
|
|
|
this.assign({ |
|
|
|
msgs: msgs.join('\n') |
|
|
|
}); |
|
|
|
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 < 90) { |
|
|
|
updateList.push(domain); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.log("==".repeat(20)) |
|
|
|
console.log('检查证书失败:', e); |
|
|
|
console.log("==".repeat(20)) |
|
|
|
} |
|
|
|
} |
|
|
|
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 { |
|
|
|
const results = await CreateSSL(domain); |
|
|
|
console.log(`证书${domain},生成成功`); |
|
|
|
} catch (error) { |
|
|
|
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(); |
|
|
|
} |
|
|
|
|
|
|
|
async createAction () { |
|
|
|
const domain = this.post('domain'); |
|
|
|
const email = this.post('email'); |
|
|
|
if (!domain || !email) { |
|
|
|
return this.fail('参数错误'); |
|
|
|
} |
|
|
|
try { |
|
|
|
const results = await CreateSSL(domain, email); |
|
|
|
this.deployCert([domain]); |
|
|
|
return this.success(results); |
|
|
|
} catch (error) { |
|
|
|
return this.fail(JSON.stringify(error)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 复制证书到指定文件夹,并重启nginx |
|
|
|
* @param {*} domains |
|
|
|
* @returns |
|
|
|
*/ |
|
|
|
async deployCert (domains) { |
|
|
|
if (!domains || domains.length === 0) { |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
const dir2 = this.config().CERT_DIR; //nginx配置文件中 |
|
|
|
// 1. 复制证书 |
|
|
|
for (let domain of domains) { |
|
|
|
const dir = path.join(PEM_PATH, domain); //生成的证书地址,项目中 |
|
|
|
let originCrt = `${dir}/${domain}.crt`; |
|
|
|
let originKey = `${dir}/${domain}.key`; |
|
|
|
if (fs.existsSync(originCrt) && fs.existsSync(originKey)) { |
|
|
|
await fs.copy(`${dir}/${domain}.crt`, `${dir2}/${domain}.crt`); |
|
|
|
await fs.copy(`${dir}/${domain}.key`, `${dir2}/${domain}.key`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 检查配置 |
|
|
|
execSync('nginx -t'); |
|
|
|
|
|
|
|
// 3. 重载服务 |
|
|
|
execSync('nginx -s reload'); |
|
|
|
console.log('证书部署成功!'); |
|
|
|
} catch (error) { |
|
|
|
console.error('部署失败:', error.message); |
|
|
|
} |
|
|
|
} |
|
|
|
} |