+
+ <%
+ const use_color = {
+ 'light': "style='color: #acb3c2 !important;'",
+ 'gray': "style='color: #79808e !important;'",
+ 'dark': "style='color: #3b4351 !important;'",
+ 'success': "style='color: #32b643 !important;'",
+ 'warning': "style='color: #ffb700 !important;'",
+ 'error': "style='color: #e85600 !important;'",
+ }
+ const use_class = {
+ 'line': "style='display: block;'",
+ 'line head': "style='display: block; margin: 0.25rem 0; font-size: .9rem;'",
+ }
+ %>
+
+
+
+
+
+ 七牛云 SSL 证书自动更新结果通知
+ >Trigger ID #<%= trigger_id %>
+ |
+
+
+
你的七牛云证书在近期进行了一次自动更新,以下是更新结果
-
<% for (const info of data) { %>
- 证书 #<%= info.old_cert_id.text %>
-
- 新证书 ID:
- <%= info.new_cert_id.text %>
+ >证书 #<%= info.old_cert_id.text %>
+ >
+ >新证书 ID:
+ ><%= info.new_cert_id.text %>
-
- 原证书 ID:
- <%= info.old_cert_id.text %>
- (<%= info.old_deleted.text %>)
+ >
+ >原证书 ID:
+ ><%= info.old_cert_id.text %>
+ >(<%= info.old_deleted.text %>)
-
- 主域名:
- <%= info.domain.text %>
+ >
+ >主域名:
+ ><%= info.domain.text %>
-
- SANS 域名:
- <%= info.sans.text %>
+ >
+ >SANS 域名:
+ ><%= info.sans.text %>
-
- 证书上传:
- <%= info.uploaded.text %>
+ >
+ >证书上传:
+ ><%= info.uploaded.text %>
-
- CDN 证书更新:
- <%= info.cdn_updated.text %>
+ >
+ >CDN 证书更新:
+ ><%= info.cdn_updated.text %>
-
- 备注:
-
+ >
+ >备注:
+ ><%= info.comment.text %>
<% } %>
- 控制台输出
- <%- output %>
-
-
-
-
+ >控制台输出
+
+ OUTPUT
+ <%- output %>
+
+ |
+
+
+ |
+
+
\ No newline at end of file
diff --git a/src/updater/qcloud.ts b/src/updater/qcloud.ts
index f0d7e8c..8a6bddf 100644
--- a/src/updater/qcloud.ts
+++ b/src/updater/qcloud.ts
@@ -1,5 +1,4 @@
-// import tencentcloud from "tencentcloud-sdk-nodejs" // compile error
-import { ssl } from "tencentcloud-sdk-nodejs"
+import * as tencentcloud from "tencentcloud-sdk-nodejs"
import SSLModule from "tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/ssl_models"
import { Client as QCloudSSLClient } from "tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/ssl_client";
import fs from "fs"
@@ -7,7 +6,7 @@ import path from "path"
import Timer from "@/utils/timer"
import { sha256, ansi2html } from "@/utils/utils"
import MailSender from "@/utils/mail-sender"
-import SSLUpdater, { SSLUpdaterOptions, sendMsgStatus } from "@/components/ssl-updater"
+import SSLUpdater, { SSLUpdaterOptions, TriggerReturnTyoe, sendMsgStatus } from "@/components/ssl-updater"
import "colors"
type StatusRecord = {
@@ -112,7 +111,7 @@ export class QCloudSSLUpdater extends SSLUpdater {
this.mailer.Template.HTML.set(fs.readFileSync(path.resolve(__dirname, "../template/qcloud.ejs"), "utf-8"));
}
- this._CLIENT = new ssl.v20191205.Client({
+ this._CLIENT = new tencentcloud.ssl.v20191205.Client({
credential: {
secretId: secretId,
secretKey: secretKey,
@@ -233,7 +232,7 @@ export class QCloudSSLUpdater extends SSLUpdater {
for (k in task_resp) {
if (!Object.prototype.hasOwnProperty.call(task_resp, k)) continue;
if (!Array.isArray(task_resp[k])) continue;
- let task_value: any = task_resp[k as keyof BindResource];
+ let task_value: any[] | undefined = task_resp[k as keyof BindResource];
if (typeof task_value === "undefined" || task_value.length === 0) continue;
else {
let val = task_value.filter((item: any) => {
@@ -248,7 +247,7 @@ export class QCloudSSLUpdater extends SSLUpdater {
res[k] = val;
}
}
- if (typeof task_info.CertId === "string") result[task_info.CertId] = null;
+ if (typeof task_info.CertId === "string") result[task_info.CertId] = res;
return resolve(void 0);
}
}, 10 * 1000)
@@ -304,8 +303,9 @@ export class QCloudSSLUpdater extends SSLUpdater {
* 触发更新
* @param domains 检测的域名列表(留空则检测所有)
*/
- async triggerUpdate(domains: string[]): Promise
{
+ async triggerUpdate(domains: string[]): Promise> {
let status_record_json: { [cert_id: string]: StatusRecord } = {};
+ let do_send_mail = false;
const fmt = (c?: number) => typeof c === "undefined" ? "?" : c.toString();
try {
const detectAll = typeof domains === "undefined" || domains.length === 0;
@@ -338,13 +338,7 @@ export class QCloudSSLUpdater extends SSLUpdater {
continue; // invalid cert info
}
- let need_continue = this._force_upload_days > 0 && (new Date(cert_info.CertEndTime).getTime() - Timer.now() < this._force_upload_days * 24 * 60 * 60 * 1000)
-
- if (!need_continue) {
- this.output.log("STEP", "UPLOAD", "SKIP", "|", "REASON", "SCHEDULE_NOT_MATCH")
- this.output.info(`Domain ${cert_info.Domain} doesn't match schedule, skip`);
- continue;
- }
+ let is_force_upload = this._force_upload_days > 0 && (new Date(cert_info.CertEndTime).getTime() - Timer.now() < this._force_upload_days * 24 * 60 * 60 * 1000)
const { public: local_pubcer, private: local_ptekey } = this._founder(cert_info.Domain, cert_info.CertSANs || [], cert_info.EncryptAlgorithm) || {};
if (typeof local_pubcer !== "string" || typeof local_ptekey !== "string") {
@@ -361,14 +355,18 @@ export class QCloudSSLUpdater extends SSLUpdater {
let is_local_changed = this.cache.is_changed([cert_info.Domain, cert_info.CertSANs, cert_info.EncryptAlgorithm], {
public: local_pubcer_sha256,
private: local_ptekey_sha256
- })
+ }, true)
+
+ let need_upload = is_force_upload || is_local_changed;
- if (!is_local_changed) {
+ if (!need_upload && !is_local_changed) {
this.output.log("STEP", "UPLOAD", "SKIP", "|", "REASON", "LOCAL_CERT_NO_CHANGE")
this.output.info(`Local certificate for domain ${cert_info.Domain} has no change, no need to update`);
continue;
}
+ this.output.log("PROCESS", "FORCE_UPLOAD", is_force_upload ? "ON" : "OFF")
+
// get detail
this.output.log("STEP", "UPLOAD[GET_OLD_DETAIL]", "START", "|", "CERT_ID", cert_info.CertificateId);
let detail_resp = await this.getCertDetail(cert_info.CertificateId);
@@ -406,6 +404,7 @@ export class QCloudSSLUpdater extends SSLUpdater {
old_deleted: null,
comment: ''
}
+ do_send_mail = true;
// upload
this.output.log("STEP", "UPLOAD[UPLOAD_NEW_CERT]", "START")
@@ -446,7 +445,10 @@ export class QCloudSSLUpdater extends SSLUpdater {
if (need_update_certificates.length === 0) {
this.output.info("No certificate needs to be updated, nothing to do");
- return Object.values(status_record_json);
+ return {
+ msg_material: Object.values(status_record_json),
+ send_mail: false // nothing to do, no need to send mail
+ }
}
// fetch bound resources
@@ -494,12 +496,14 @@ export class QCloudSSLUpdater extends SSLUpdater {
resources: resource_types as ResourceType[],
regions: resource_regions
})
- this.output.log("STEP", "UPDATE[UPDATE_CERT_INSTANCE]", "DONE", "|", "STATUS", update_resp.DeployStatus);
+ this.output.log("STEP", "UPDATE[UPDATE_CERT_INSTANCE]", "DONE", "|", "STATUS", update_resp.DeployStatus, "|", "RECORD_ID", update_resp.DeployRecordId);
- if (update_resp.DeployStatus !== 1) {
- this.output.log("STEP", "UPDATE", "SKIP", "|", "REASON", "DEPLOY_FAILED".red)
- this.output.failure(`Failed to update resources for certificate ${cert_info.CertificateId} (domain: ${cert_info.Domain}) (${cert_info.Alias})`);
- continue;
+ if (update_resp.DeployStatus !== 1) { // 部署失败
+ if (typeof update_resp.DeployRecordId !== "number" || update_resp.DeployRecordId < 0) { // 是否是正在进行中
+ this.output.log("STEP", "UPDATE", "SKIP", "|", "REASON", "DEPLOY_FAILED".red)
+ this.output.failure(`Failed to update resources for certificate ${cert_info.CertificateId} (domain: ${cert_info.Domain}) (${cert_info.Alias})`);
+ continue;
+ }
}
this.output.log("STEP", "UPDATE", "DONE");
@@ -520,7 +524,10 @@ export class QCloudSSLUpdater extends SSLUpdater {
}
}
} catch (err) { this.output.error(err); }
- return Object.values(status_record_json)
+ return {
+ msg_material: Object.values(status_record_json),
+ send_mail: do_send_mail
+ }
}
async sendMsg(title: string, content: string): Promise {
diff --git a/src/updater/qiniu.ts b/src/updater/qiniu.ts
index c62d937..511da75 100644
--- a/src/updater/qiniu.ts
+++ b/src/updater/qiniu.ts
@@ -3,7 +3,7 @@ import path from "path"
import Timer from "@/utils/timer"
import { sha256, ansi2html, hmacSha1 } from "@/utils/utils"
import MailSender from "@/utils/mail-sender"
-import SSLUpdater, { SSLUpdaterOptions, CertificateData, sendMsgStatus } from "@/components/ssl-updater"
+import SSLUpdater, { SSLUpdaterOptions, CertificateData, sendMsgStatus, TriggerReturnTyoe } from "@/components/ssl-updater"
import urllib from "urllib"
import "colors"
@@ -77,11 +77,11 @@ export namespace QiniuAPI {
*/
create_time: number
/**
- * 证书生效时间
+ * 证书生效时间(单位:秒)
*/
not_before: number
/**
- * 证书过期时间
+ * 证书过期时间(单位:秒)
*/
not_after: number
/**
@@ -156,11 +156,11 @@ export namespace QiniuAPI {
*/
create_time: number
/**
- * 证书生效时间
+ * 证书生效时间(单位:秒)
*/
not_before: number
/**
- * 证书过期时间
+ * 证书过期时间(单位:秒)
*/
not_after: number
/**
@@ -767,8 +767,9 @@ export class QiniuSSLUpdater extends SSLUpdater {
})
}
- async triggerUpdate(domains: string[]): Promise {
+ async triggerUpdate(domains: string[]): Promise> {
let status_record_json: { [cert_id: string]: StatusRecord } = {};
+ let do_send_mail = false
const fmt = (c?: number) => typeof c === "undefined" ? "?" : c.toString();
try {
const detectAll = typeof domains === "undefined" || domains.length === 0;
@@ -795,13 +796,7 @@ export class QiniuSSLUpdater extends SSLUpdater {
"|", "DOMAIN", cert_info.common_name,
"|", "ALIAS", cert_info.name);
- let need_continue = this._force_upload_days > 0 && (new Date(cert_info.not_after).getTime() - Timer.now() < this._force_upload_days * 24 * 60 * 60 * 1000)
-
- if (!need_continue) {
- this.output.log("STEP", "UPLOAD", "SKIP", "|", "REASON", "SCHEDULE_NOT_MATCH");
- this.output.info(`Domain ${cert_info.common_name} doesn't match schedule, skip`);
- continue;
- }
+ let is_force_upload = this._force_upload_days > 0 && (new Date(cert_info.not_after * 1000).getTime() - Timer.now() < this._force_upload_days * 24 * 60 * 60 * 1000)
const { public: local_pubcer, private: local_ptekey } = this._founder(cert_info.common_name, cert_info.dnsnames, cert_info.encrypt);
if (typeof local_pubcer !== "string" || typeof local_ptekey !== "string") {
@@ -818,14 +813,18 @@ export class QiniuSSLUpdater extends SSLUpdater {
let is_local_changed = this.cache.is_changed([cert_info.common_name, cert_info.dnsnames, cert_info.encrypt], {
public: local_pubcer_sha256,
private: local_ptekey_sha256
- })
+ }, true)
- if (!is_local_changed) {
+ let need_upload = is_force_upload || is_local_changed
+
+ if (!need_upload && !is_local_changed) {
this.output.log("STEP", "UPLOAD", "SKIP", "|", "REASON", "LOCAL_CERT_NO_CHANGE");
this.output.info(`Local certificate for domain ${cert_info.common_name} has no change, no need to update`);
continue;
}
+ this.output.log("PROCESS", "FORCE_UPLOAD", is_force_upload ? "ON" : "OFF")
+
// get detail
this.output.log("STEP", "UPLOAD[GET_OLD_DETAIL]", "START", "|", "OLD_CERT_ID", cert_info.certid)
let detail_resp = await this.getCertDetail(cert_info.certid);
@@ -861,6 +860,7 @@ export class QiniuSSLUpdater extends SSLUpdater {
old_deleted: null,
comment: ''
}
+ do_send_mail = true
// upload
this.output.log("STEP", "UPLOAD[UPLOAD_NEW_CERT]", "START");
@@ -1013,7 +1013,10 @@ export class QiniuSSLUpdater extends SSLUpdater {
}
await Promise.all(delete_promise_pool);
} catch (err) { this.output.error(err); }
- return Object.values(status_record_json);
+ return {
+ msg_material: Object.values(status_record_json),
+ send_mail: do_send_mail
+ }
}
async sendMsg(title: string, content: string): Promise {
diff --git a/src/utils/cache.ts b/src/utils/cache.ts
index e763d43..cf0faf9 100644
--- a/src/utils/cache.ts
+++ b/src/utils/cache.ts
@@ -12,7 +12,9 @@ export class Cache {
return this.cache[JSON.stringify(key)]
}
- is_changed(key: any, value: any) {
+ is_changed(key: any, value: any, strict = true) {
+ const cache_key = JSON.stringify(key)
+ if (!Object.prototype.hasOwnProperty.call(this.cache, cache_key)) return strict ? true : false
return this._different(this.cache[JSON.stringify(key)], value)
}
diff --git a/src/utils/timer.ts b/src/utils/timer.ts
index 93b357f..b5e118f 100644
--- a/src/utils/timer.ts
+++ b/src/utils/timer.ts
@@ -1,10 +1,24 @@
type DateLike = number | string | Date
+type TimerWatchCallBack = (run_time: number) => T
+type TimerWatchOptions = {
+ /**
+ * Prevent the callback from being called when the former callback is still running
+ * @default true
+ */
+ preventWhenRunning?: boolean
+ /**
+ * Run the callback instantly when the timer is started
+ * @default false
+ */
+ InstantlyRun?: boolean
+}
+
export class Timer {
private _start_time: number;
private _interval: number;
- public record: number;
+ public appoint: number;
constructor(start_time: DateLike, interval: number) {
@@ -13,39 +27,99 @@ export class Timer {
this._interval = interval;
if (typeof this._interval !== "number") throw new TypeError("interval must be a number");
else this._interval = Math.abs(this._interval);
- // record 只会记录 interval 到达的时间,并且不会自动更新
- // 自动更新需要外部使用 setInterval 不断调用 set_future
- if (Timer.now() < this._start_time) this.record = this._start_time;
- else this.record = Math.floor((Timer.now() - this._start_time) / this._interval) * this._interval + this._start_time;
+
+ this.nearest_start_time();
+ this.appoint = this.future();
}
static now() {
return Date.now ? Date.now() : new Date().getTime()
}
+ /**
+ * Update start time to what is most close to now
+ */
+ nearest_start_time() {
+ let nowTime = Timer.now();
+ if (nowTime < this._start_time) return this._start_time; // future, waiting for its coming
+ let possible = Math.floor((nowTime - this._start_time) / this._interval) * this._interval + this._start_time;
+ if (possible > nowTime) possible -= this._interval;
+ return this._start_time = possible;
+ }
+
+ /**
+ * Watch the timer and call the callback when the time is up
+ */
+ watch(callback: TimerWatchCallBack, opts: TimerWatchOptions) {
+ if (!Object.prototype.hasOwnProperty.call(opts, "preventWhenRunning")) {
+ opts.preventWhenRunning = true;
+ }
+ if (!Object.prototype.hasOwnProperty.call(opts, "InstantlyRun")) {
+ opts.InstantlyRun = false;
+ }
+
+ function isAsync(fn: Function): fn is TimerWatchCallBack> {
+ return typeof fn === "function" && fn.constructor.name === "AsyncFunction";
+ }
+
+ let system_prevent = false; // prevent when one schedule is running
+ let user_prevent = false; // only prevent calling the callback
+
+ let force_run = Boolean(opts.InstantlyRun);
+ const itvid = setInterval(() => {
+ if (system_prevent) return;
+ system_prevent = true;
+
+ if (opts.preventWhenRunning && user_prevent) return;
+
+ if (this.is_expired() || force_run) {
+ force_run = false;
+ let callingTime = this.appoint;
+ this.set_future();
+ this.nearest_start_time();
+ system_prevent = false;
+
+ user_prevent = true;
+ // The parameter `callback` might be delivered by reference
+ // That means it might be changed in calls at different calling time
+ // So we need to check it (async or sync) every time before calling it
+ if (isAsync(callback)) {
+ callback(callingTime).finally(() => {
+ user_prevent = false;
+ })
+ } else if (typeof callback === "function") {
+ callback(callingTime);
+ user_prevent = false;
+ }
+ } else {
+ system_prevent = false;
+ }
+ }, 1000);
+ return itvid;
+ }
+
next() {
- return this.record += this._interval;
+ return this.appoint + this._interval;
}
future() {
- if (Timer.now() < this._start_time) return this._start_time;
- let result = (Math.floor((Timer.now() - this._start_time) / this._interval) + 1) * this._interval + this._start_time;
- // ensure no bugs
- let now = Timer.now();
- while (result < now) result += this._interval;
+ let nowTime = Timer.now();
+ if (nowTime < this._start_time) return this._start_time; // future, waiting for its coming
+ let result = Math.floor((nowTime - this._start_time) / this._interval) * this._interval + this._start_time;
+ while (result <= nowTime) result += this._interval;
return result;
}
is_expired() {
- return this.record < Timer.now();
+ return this.appoint < Timer.now();
}
set_next() {
- return this.record = this.next();
+ return this.appoint = this.next();
}
set_future() {
- return this.record = this.future();
+ return this.appoint = this.future();
}
}