perf: http校验方式,支持七牛云oss、阿里云oss、腾讯云cos

v2
xiaojunnuo 2025-01-04 01:45:24 +08:00
parent 297d09c5ad
commit 3f74d4d9e5
23 changed files with 385 additions and 73 deletions

View File

@ -186,11 +186,12 @@ export class AcmeService {
const filePath = `.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
this.logger.info(`校验 ${fullDomain} ,准备上传文件:${filePath}`);
await httpUploader.upload(filePath, fileContents);
await httpUploader.upload(filePath, Buffer.from(fileContents));
this.logger.info(`上传文件【${filePath}】成功`);
return {
challenge,
keyAuthorization,
httpUploader,
};
};
@ -248,7 +249,8 @@ export class AcmeService {
const httpVerifyPlan = domainVerifyPlan.httpVerifyPlan;
if (httpVerifyPlan) {
const httpChallenge = getChallenge("http-01");
return await doHttpVerify(httpChallenge, httpVerifyPlan[fullDomain].httpUploader);
const plan = httpVerifyPlan[fullDomain];
return await doHttpVerify(httpChallenge, plan.httpUploader);
} else {
throw new Error("未找到域名【" + fullDomain + "】的http校验配置");
}

View File

@ -416,9 +416,14 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
for (const key in domainVerifyPlan.httpVerifyPlan) {
const httpRecord = domainVerifyPlan.httpVerifyPlan[key];
const access = await this.ctx.accessService.getById(httpRecord.httpUploaderAccess);
let rootDir = httpRecord.httpUploadRootDir;
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
rootDir = rootDir + "/";
}
this.logger.info("上传方式", httpRecord.httpUploaderType);
const httpUploader = await httpChallengeUploaderFactory.createUploaderByType(httpRecord.httpUploaderType, {
access,
rootDir: httpRecord.httpUploadRootDir,
rootDir: rootDir,
ctx: httpUploaderContext,
});
httpVerifyPlan[key] = {

View File

@ -2,7 +2,7 @@ import { IAccessService } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
export type HttpChallengeUploader = {
upload: (fileName: string, fileContent: string) => Promise<void>;
upload: (fileName: string, fileContent: Buffer) => Promise<void>;
remove: (fileName: string) => Promise<void>;
};
@ -31,5 +31,5 @@ export abstract class BaseHttpChallengeUploader<A> implements HttpChallengeUploa
}
abstract remove(fileName: string): Promise<void>;
abstract upload(fileName: string, fileContent: string): Promise<void>;
abstract upload(fileName: string, fileContent: Buffer): Promise<void>;
}

View File

@ -3,7 +3,7 @@ import { AliossAccess, AliyunAccess } from "@certd/plugin-lib";
import { AliossClient } from "@certd/plugin-lib";
export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<AliossAccess> {
async upload(filePath: string, fileContent: string) {
async upload(filePath: string, fileContent: Buffer) {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
@ -11,16 +11,19 @@ export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<Alios
region: this.access.region,
});
await client.uploadFile(filePath, Buffer.from(fileContent));
const key = this.rootDir + filePath;
this.logger.info(`开始上传文件: ${key}`);
await client.uploadFile(key, fileContent);
this.logger.info(`校验文件上传成功: ${filePath}`);
}
async remove(filePath: string) {
const key = this.rootDir + filePath;
// remove file from alioss
const client = await this.getAliossClient();
await client.removeFile(filePath);
this.logger.info(`文件删除成功: ${filePath}`);
await client.removeFile(key);
this.logger.info(`文件删除成功: ${key}`);
}
private async getAliossClient() {

View File

@ -1,24 +1,41 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { FtpAccess, FtpClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";
import fs from "fs";
export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
async upload(fileName: string, fileContent: string) {
async upload(filePath: string, fileContent: Buffer) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
await client.upload(fileName, fileContent);
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
try {
// Write file to temp path
const path = this.rootDir + filePath;
await client.upload(path, tmpFilePath);
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
});
}
async remove(fileName: string) {
async remove(filePath: string) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
await client.client.remove(fileName);
const path = this.rootDir + filePath;
await client.client.remove(path);
});
}
}

View File

@ -1,10 +1,31 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { QiniuOssAccess } from "@certd/plugin-lib/dist/qiniu/access-oss";
import { QiniuOssAccess, QiniuClient, QiniuAccess } from "@certd/plugin-lib";
export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<QiniuOssAccess> {
async upload(fileName: string, fileContent: string) {
return null;
async upload(filePath: string, fileContent: Buffer) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.uploadFile(this.access.bucket, this.rootDir + filePath, fileContent);
}
async remove(fileName: string) {}
async remove(filePath: string) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.removeFile(this.access.bucket, this.rootDir + filePath);
}
}

View File

@ -5,11 +5,17 @@ import os from "os";
import fs from "fs";
export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAccess> {
async upload(fileName: string, fileContent: string) {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", fileName);
async upload(filePath: string, fileContent: Buffer) {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
// Write file to temp path
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
const key = this.rootDir + filePath;
try {
const client = new SshClient(this.logger);
await client.uploadFiles({
@ -17,8 +23,8 @@ export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAcces
mkdirs: true,
transports: [
{
localPath: fileName,
remotePath: fileName,
localPath: tmpFilePath,
remotePath: key,
},
],
});
@ -30,9 +36,10 @@ export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAcces
async remove(filePath: string) {
const client = new SshClient(this.logger);
const key = this.rootDir + filePath;
await client.removeFiles({
connectConf: this.access,
files: [filePath],
files: [key],
});
}
}

View File

@ -1,10 +1,28 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { TencentCosAccess } from "@certd/plugin-lib/dist/tencent/access-cos";
import { TencentAccess, TencentCosAccess, TencentCosClient } from "@certd/plugin-lib";
export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<TencentCosAccess> {
async upload(fileName: string, fileContent: string) {
return null;
async upload(filePath: string, fileContent: Buffer) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.uploadFile(key, fileContent);
}
async remove(fileName: string) {}
async remove(filePath: string) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.removeFile(key);
}
}

View File

@ -21,14 +21,17 @@
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.21.0",
"basic-ftp": "^5.0.5",
"cos-nodejs-sdk-v5": "^2.14.6",
"dayjs": "^1.11.7",
"iconv-lite": "^0.6.3",
"lodash-es": "^4.17.21",
"qiniu": "^7.12.0",
"rimraf": "^5.0.5",
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
"ssh2": "^1.15.0",
"strip-ansi": "^7.1.0"
"strip-ansi": "^7.1.0",
"tencentcloud-sdk-nodejs": "^4.0.1005"
},
"devDependencies": {
"@types/chai": "^4.3.3",

View File

@ -1 +1,3 @@
export * from "./access.js";
export * from "./access-oss.js";
export * from "./lib/sdk.js";

View File

@ -0,0 +1,142 @@
import { HttpClient, ILogger } from "@certd/basic";
import { QiniuAccess } from "../access.js";
export type QiniuCertInfo = {
key: string;
crt: string;
};
export class QiniuClient {
http: HttpClient;
access: QiniuAccess;
logger: ILogger;
constructor(opts: { http: HttpClient; access: QiniuAccess; logger: ILogger }) {
this.http = opts.http;
this.access = opts.access;
this.logger = opts.logger;
}
async uploadCert(cert: QiniuCertInfo, certName?: string) {
const url = "https://api.qiniu.com/sslcert";
const body = {
name: certName,
common_name: "certd",
pri: cert.key,
ca: cert.crt,
};
const res = await this.doRequest(url, "post", body);
return res.certID;
}
async bindCert(body: { certid: string; domain: string }) {
const url = "https://api.qiniu.com/cert/bind";
return await this.doRequest(url, "post", body);
}
async getCertBindings() {
const url = "https://api.qiniu.com/cert/bindings";
const res = await this.doRequest(url, "get");
return res;
}
async doRequest(url: string, method: string, body?: any) {
const { generateAccessToken } = await import("qiniu/qiniu/util.js");
const token = generateAccessToken(this.access, url);
const res = await this.http.request({
url,
method: method,
headers: {
Authorization: token,
},
data: body,
logRes: false,
});
if (res && res.error) {
if (res.error.includes("domaintype")) {
throw new Error("请求失败:" + res.error + ",该域名属于CDN域名请使用部署到七牛云CDN插件");
}
throw new Error("请求失败:" + res.error);
}
console.log("res", res);
return res;
}
async doRequestV2(opts: { url: string; method: string; body?: any; contentType: string }) {
const { HttpClient } = await import("qiniu/qiniu/httpc/client.js");
const { QiniuAuthMiddleware } = await import("qiniu/qiniu/httpc/middleware/qiniuAuth.js");
// X-Qiniu-Date: 20060102T150405Z
const auth = new QiniuAuthMiddleware({
mac: {
...this.access,
options: {},
},
});
const http = new HttpClient({ timeout: 10000, middlewares: [auth] });
console.log("http", http);
return new Promise((resolve, reject) => {
try {
http.get({
url: opts.url,
headers: {
"Content-Type": opts.contentType,
},
callback: (nullable, res) => {
console.log("nullable", nullable, "res", res);
if (res?.error) {
reject(res);
} else {
resolve(res);
}
},
});
} catch (e) {
reject(e);
}
});
}
async uploadFile(bucket: string, key: string, content: Buffer) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const options = {
scope: bucket,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
// 文件上传
const { data, resp } = await formUploader.put(uploadToken, key, content, putExtra);
if (resp.statusCode === 200) {
this.logger.info("文件上传成功:" + key);
return data;
} else {
console.log(resp.statusCode);
throw new Error("上传失败:" + JSON.stringify(resp));
}
}
async removeFile(bucket: string, key: string) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const config = new qiniu.conf.Config();
config.useHttpsDomain = true;
const bucketManager = new qiniu.rs.BucketManager(mac, config);
const { resp } = await bucketManager.delete(bucket, key);
if (resp.statusCode === 200) {
this.logger.info("文件删除成功:" + key);
return;
} else {
throw new Error("删除失败:" + JSON.stringify(resp));
}
}
}

View File

@ -61,3 +61,5 @@ export class TencentCosAccess extends BaseAccess {
})
bucket = "";
}
new TencentCosAccess();

View File

@ -1 +1,3 @@
export * from "./access.js";
export * from "./access-cos.js";
export * from "./lib/index.js";

View File

@ -0,0 +1,69 @@
import { TencentAccess } from "../access.js";
import { ILogger } from "@certd/basic";
export class TencentCosClient {
access: TencentAccess;
logger: ILogger;
region: string;
bucket: string;
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) {
this.access = opts.access;
this.logger = opts.logger;
this.bucket = opts.bucket;
this.region = opts.region;
}
async getCosClient() {
const sdk = await import("cos-nodejs-sdk-v5");
const clientConfig = {
SecretId: this.access.secretId,
SecretKey: this.access.secretKey,
};
return new sdk.default(clientConfig);
}
async uploadFile(key: string, file: Buffer) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
cos.putObject(
{
Bucket: this.bucket /* 必须 */,
Region: this.region /* 必须 */,
Key: key /* 必须 */,
Body: file, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
},
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
}
);
});
}
async removeFile(key: string) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
cos.deleteObject(
{
Bucket: this.bucket,
Region: this.region,
Key: key,
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
}
);
});
}
}

View File

@ -0,0 +1,2 @@
export * from "./ssl-client.js";
export * from "./cos-client.js";

View File

@ -1,6 +1,10 @@
import { TencentAccess } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
import { ILogger } from '@certd/basic';
import { ILogger } from "@certd/basic";
import { TencentAccess } from "../access.js";
export type TencentCertInfo = {
key: string;
crt: string;
};
export class TencentSslClient {
access: TencentAccess;
logger: ILogger;
@ -11,7 +15,7 @@ export class TencentSslClient {
this.region = opts.region;
}
async getSslClient(): Promise<any> {
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js');
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
const SslClient = sdk.v20191205.Client;
const clientConfig = {
@ -22,7 +26,7 @@ export class TencentSslClient {
region: this.region,
profile: {
httpProfile: {
endpoint: 'ssl.tencentcloudapi.com',
endpoint: "ssl.tencentcloudapi.com",
},
},
};
@ -32,11 +36,11 @@ export class TencentSslClient {
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error('请求失败:' + ret.Error.Code + ',' + ret.Error.Message);
throw new Error("请求失败:" + ret.Error.Code + "," + ret.Error.Message);
}
}
async uploadToTencent(opts: { certName: string; cert: CertInfo }): Promise<string> {
async uploadToTencent(opts: { certName: string; cert: TencentCertInfo }): Promise<string> {
const client = await this.getSslClient();
const params = {
CertificatePublicKey: opts.cert.crt,
@ -45,7 +49,7 @@ export class TencentSslClient {
};
const ret = await client.UploadCertificate(params);
this.checkRet(ret);
this.logger.info('证书上传成功tencentCertId=', ret.CertificateId);
this.logger.info("证书上传成功tencentCertId=", ret.CertificateId);
return ret.CertificateId;
}

View File

@ -52,6 +52,7 @@ watch(
},
(value: any) => {
if (value) {
debugger;
records.value = {
...value
};

View File

@ -138,18 +138,7 @@ function showError(error: string) {
type DomainGroup = Record<string, Record<string, CnameRecord>>;
watch(
() => {
return props.defaultType;
},
(value: string) => {
planRef.value = {};
onDomainsChanged(props.domains);
}
);
function onDomainsChanged(domains: string[]) {
console.log("域名变化", domains);
if (domains == null) {
return;
}
@ -183,22 +172,46 @@ function onDomainsChanged(domains: string[]) {
//@ts-ignore
type: props.defaultType || "cname",
//@ts-ignore
cnameVerifyPlan: {
...subDomains
},
cnameVerifyPlan: {},
//@ts-ignore
httpVerifyPlan: {
...subDomains
}
httpVerifyPlan: {}
};
planRef.value[domain] = planItem;
}
const cnameOrigin = planItem.cnameVerifyPlan;
const httpOrigin = planItem.httpVerifyPlan;
planItem.cnameVerifyPlan = {};
planItem.httpVerifyPlan = {};
for (const subDomain in subDomains) {
if (!cnameOrigin[subDomain]) {
//@ts-ignore
planItem.cnameVerifyPlan[subDomain] = {
id: 0
};
} else {
planItem.cnameVerifyPlan[subDomain] = cnameOrigin[subDomain];
}
}
for (const subDomain in subDomains) {
if (!httpOrigin[subDomain]) {
//@ts-ignore
planItem.httpVerifyPlan[subDomain] = {
domain: subDomain
};
} else {
planItem.httpVerifyPlan[subDomain] = httpOrigin[subDomain];
}
}
const cnamePlan = planItem.cnameVerifyPlan;
for (const subDomain in subDomains) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
};
if (!cnamePlan[subDomain]) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
};
}
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!subDomains[subDomain]) {
@ -209,10 +222,13 @@ function onDomainsChanged(domains: string[]) {
// httpVerifyPlan
const httpPlan = planItem.httpVerifyPlan;
for (const subDomain in subDomains) {
//@ts-ignore
httpPlan[subDomain] = {
domain: subDomain
};
debugger;
if (!httpPlan[subDomain]) {
//@ts-ignore
httpPlan[subDomain] = {
domain: subDomain
};
}
}
for (const subDomain of Object.keys(httpPlan)) {
if (!subDomains[subDomain]) {
@ -226,14 +242,15 @@ function onDomainsChanged(domains: string[]) {
delete planRef.value[domain];
}
}
debugger;
}
watch(
() => {
return props.domains;
return props.domains && props.defaultType;
},
(domains: string[]) => {
onDomainsChanged(domains);
() => {
onDomainsChanged(props.domains);
},
{
immediate: true,

View File

@ -1,8 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess } from '@certd/plugin-lib';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess, QiniuClient } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { QiniuClient } from '@certd/plugin-plus';
@IsTaskPlugin({
name: 'QiniuDeployCertToCDN',

View File

@ -1,7 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { QiniuClient } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { QiniuAccess } from '@certd/plugin-lib';
import { QiniuAccess, QiniuClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'QiniuCertUpload',

View File

@ -1,9 +1,8 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
import { TencentSslClient } from '../../lib/index.js';
import dayjs from 'dayjs';
import { remove } from 'lodash-es';
import { TencentAccess } from '@certd/plugin-lib';
import { TencentAccess, TencentSslClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'TencentDeleteExpiringCert',

View File

@ -1,8 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { TencentSslClient } from '../../lib/index.js';
import { createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { TencentAccess } from '@certd/plugin-lib';
import { TencentAccess, TencentSslClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'TencentDeployCertToCDNv2',
title: '腾讯云-部署到CDN-v2',

View File

@ -1,7 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { TencentSslClient } from '../../lib/index.js';
import { createRemoteSelectInputDefine, TencentSslClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'DeployCertToTencentCosPlugin',