diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 6af570c0..6e658382 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -87,3 +87,15 @@ jobs: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 greper/certd:armv7 greper/certd:${{steps.get_certd_version.outputs.result}}-armv7 + + - name: Build agent + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + context: ./packages/ui/agent/ + tags: | + registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest + registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}} + greper/certd-agent:latest + greper/certd-agent:${{steps.get_certd_version.outputs.result}} diff --git a/doc/dev/development.md b/doc/dev/development.md index dc6920e1..ba918ac0 100644 --- a/doc/dev/development.md +++ b/doc/dev/development.md @@ -3,7 +3,7 @@ ## 1.本地调试运行 -安装依赖包: +### 克隆代码 ```shell # 克隆代码 @@ -11,22 +11,38 @@ git clone https://github.com/certd/certd #进入项目目录 cd certd +``` +### 修改pnpm-workspace.yaml文件 +重要:否则无法正确加载专业版的access和plugin +```yaml +# pnpm-workspace.yaml +packages: + - 'packages/**' # <--------------注释掉这一行,PR时不要提交此修改 + - 'packages/ui/**' +``` + +### 安装依赖和初始化: +```shell +# 安装pnpm,如果提示npm命令不存在,就需要先安装nodejs +npm install -g pnpm@8.15.7 --registry=https://registry.npmmirror.com + +# 使用国内镜像源,如果有代理,就不需要 +pnpm config set registry https://registry.npmmirror.com # 安装依赖 -npm install -g pnpm@8.15.7 pnpm install # 初始化构建 npm run init ``` -启动 server: +### 启动 server: ```shell cd packages/ui/certd-server npm run dev ``` -启动 client: +### 启动 client: ```shell cd packages/ui/certd-client npm run dev @@ -48,7 +64,7 @@ npm run dev 这样用户就可以在`certd`后台中创建这种授权凭证了 ### 3. dns-provider -如果域名是这个平台进行解析的,那么你需要实现dns-provider +如果域名是这个平台进行解析的,那么你需要实现dns-provider,(申请证书需要) 参考`plugin-cloudflare/dns-provider.ts` 修改为你要做的平台的`dns-provider` ### 4. plugin-deploy @@ -66,7 +82,7 @@ export * from './plugins/plugin-deploy-to-xx' 在`./src/plugins/index.ts`中增加`import` ```ts -export * from "./plugin-cloudflare" +export * from "./plugin-cloudflare.js" ``` ## 重启服务进行调试 diff --git a/doc/google/google.md b/doc/google/google.md index f77a8bba..7d880431 100644 --- a/doc/google/google.md +++ b/doc/google/google.md @@ -7,16 +7,18 @@ https://console.cloud.google.com/apis/library/publicca.googleapis.com 打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。 -## 2、 申请Key -随后打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。 +## 2、 获取授权 +以下两种方式任选其一 +### 2.1 直接获取EAB 【推荐】 + +1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。 等待分配完成后在 Shell 窗口内输入如下命令: ```shell gcloud beta publicca external-account-keys create ``` -此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。 - +2. 此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。 执行完成后会返回类似如下输出;注意不要在没有收到 Google 的邮件时执行该命令,会返回命令不存在。 ```shell @@ -24,14 +26,31 @@ Created an external account key [b64MacKey: xxxxxxxxxxxxxxxx keyId: xxxxxxxxxxxxx] ``` -记录以上信息备用(注意keyId是不带中括号的) + +3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息 + 注意:keyId没有`]`结尾,不要把`]`也复制了 + +注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱 +否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation` + +### 2.2 通过服务账号获取EAB + +此方式可以自动EAB,需要配置代理 + +1. 创建服务账号 +https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1 + +2. 选择一个项目,进入创建服务账号页面 +3. 给服务账号起一个名字,点击`创建并继续` +4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner` +5. 点击完成 +6. 点击服务账号,进入服务账号详情页面 +7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件 +8. 将json文件内容粘贴到 certd中 Google服务授权输入框中 ## 3、 创建证书流水线 -选择证书提供商为google, 开启使用代理 +选择证书提供商为google, 选择EAB授权 或 服务账号授权 -## 4、 将key信息作为EAB授权信息 -google证书需要EAB授权, 使用第二步中的 keyId 和 b64MacKey 信息创建一条EAB授权记录 -注意:keyId没有`]`结尾,不要把`]`也复制了 -## 5、 其他就跟正常申请证书一样了 +## 4、 其他就跟正常申请证书一样了 diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts index 0ade2738..6bfa1e85 100644 --- a/packages/libs/lib-server/src/system/settings/service/models.ts +++ b/packages/libs/lib-server/src/system/settings/service/models.ts @@ -54,3 +54,11 @@ export class SysSiteInfo extends BaseSettings { logo?: string; loginLogo?: string; } + +export class SysSiteEnv { + agent?: { + enabled?: boolean; + contactText?: string; + contactLink?: string; + }; +} diff --git a/packages/plugins/plugin-cert/package.json b/packages/plugins/plugin-cert/package.json index 79e19a1b..f90dd780 100644 --- a/packages/plugins/plugin-cert/package.json +++ b/packages/plugins/plugin-cert/package.json @@ -14,9 +14,10 @@ "preview": "vite preview" }, "dependencies": { - "@certd/basic": "^1.25.9", "@certd/acme-client": "^1.25.9", + "@certd/basic": "^1.25.9", "@certd/pipeline": "^1.25.9", + "@google-cloud/publicca": "^1.3.0", "dayjs": "^1.11.7", "jszip": "^3.10.1", "node-forge": "^0.10.0", diff --git a/packages/plugins/plugin-cert/src/access/google-access.ts b/packages/plugins/plugin-cert/src/access/google-access.ts new file mode 100644 index 00000000..efc66355 --- /dev/null +++ b/packages/plugins/plugin-cert/src/access/google-access.ts @@ -0,0 +1,97 @@ +import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline"; + +@IsAccess({ + name: "google", + title: "google cloud", + desc: "谷歌云授权", +}) +export class GoogleAccess extends BaseAccess { + @AccessInput({ + title: "密钥类型", + value: "serviceAccount", + component: { + placeholder: "密钥类型", + name: "a-select", + vModel: "value", + options: [ + { value: "serviceAccount", label: "服务账号密钥" }, + { value: "apiKey", label: "ApiKey,暂不可用", disabled: true }, + ], + }, + helper: "密钥类型", + required: true, + encrypt: false, + }) + type = ""; + + @AccessInput({ + title: "项目ID", + component: { + placeholder: "ProjectId", + }, + helper: "ProjectId", + required: true, + encrypt: false, + mergeScript: ` + return { + show:ctx.compute(({form})=>{ + return form.access.type === 'apiKey' + }) + } + `, + }) + projectId = ""; + + @AccessInput({ + title: "ApiKey", + component: { + placeholder: "ApiKey", + }, + helper: "不要选,目前没有用", + required: true, + encrypt: true, + mergeScript: ` + return { + show:ctx.compute(({form})=>{ + return form.access.type === 'apiKey' + }) + } + `, + }) + apiKey = ""; + + @AccessInput({ + title: "服务账号密钥", + component: { + placeholder: "serviceAccountSecret", + name: "a-textarea", + vModel: "value", + rows: 4, + }, + helper: + "[如何创建服务账号](https://cloud.google.com/iam/docs/service-accounts-create?hl=zh-CN) \n[获取密钥](https://console.cloud.google.com/iam-admin/serviceaccounts?hl=zh-cn),点击详情,点击创建密钥,将下载json文件,把内容填在此处", + required: true, + encrypt: true, + mergeScript: ` + return { + show:ctx.compute(({form})=>{ + return form.access.type === 'serviceAccount' + }) + } + `, + }) + serviceAccountSecret = ""; + + @AccessInput({ + title: "https代理", + component: { + placeholder: "http://127.0.0.1:10811", + }, + helper: "Google的请求需要走代理,如果不配置,则会使用环境变量中的全局HTTPS_PROXY配置\n或者服务器本身在海外,则不需要配置", + required: false, + encrypt: false, + }) + httpsProxy = ""; +} + +new GoogleAccess(); diff --git a/packages/plugins/plugin-cert/src/access/index.ts b/packages/plugins/plugin-cert/src/access/index.ts index 4111cb7d..0ea7960a 100644 --- a/packages/plugins/plugin-cert/src/access/index.ts +++ b/packages/plugins/plugin-cert/src/access/index.ts @@ -1 +1,2 @@ export * from "./eab-access.js"; +export * from "./google-access.js"; diff --git a/packages/plugins/plugin-cert/src/libs/google.ts b/packages/plugins/plugin-cert/src/libs/google.ts new file mode 100644 index 00000000..8fea2f68 --- /dev/null +++ b/packages/plugins/plugin-cert/src/libs/google.ts @@ -0,0 +1,62 @@ +import { EabAccess, GoogleAccess } from "../access/index.js"; +import { ILogger } from "@certd/basic"; + +export class GoogleClient { + access: GoogleAccess; + logger: ILogger; + constructor(opts: { logger: ILogger; access: GoogleAccess }) { + this.access = opts.access; + this.logger = opts.logger; + } + async getEab() { + // https://cloud.google.com/docs/authentication/api-keys-use#using-with-client-libs + const { v1 } = await import("@google-cloud/publicca"); + // process.env.HTTPS_PROXY = "http://127.0.0.1:10811"; + const access = this.access; + if (!access.serviceAccountSecret) { + throw new Error("服务账号密钥 不能为空"); + } + const credentials = JSON.parse(access.serviceAccountSecret); + + const client = new v1.PublicCertificateAuthorityServiceClient({ credentials }); + const parent = `projects/${credentials.project_id}/locations/global`; + const externalAccountKey = {}; + const request = { + parent, + externalAccountKey, + }; + + let envHttpsProxy = ""; + try { + if (this.access.httpsProxy) { + //设置临时使用代理 + envHttpsProxy = process.env.HTTPS_PROXY; + process.env.HTTPS_PROXY = this.access.httpsProxy; + } + this.logger.info("开始获取google eab授权"); + const response = await client.createExternalAccountKey(request); + const { keyId, b64MacKey } = response[0]; + const eabAccess = new EabAccess(); + eabAccess.kid = keyId; + eabAccess.hmacKey = b64MacKey.toString(); + this.logger.info(`google eab授权获取成功,kid: ${eabAccess.kid}`); + return eabAccess; + } finally { + if (envHttpsProxy) { + process.env.HTTPS_PROXY = envHttpsProxy; + } + } + } +} + +// const access = new GoogleAccess(); +// access.projectId = "hip-light-432411-d4"; +// access.serviceAccountSecret = ` +// +// +// `; +// // process.env.HTTPS_PROXY = "http://127.0.0.1:10811"; +// const client = new GoogleClient(access); +// client.getEab().catch((e) => { +// console.error(e); +// }); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index 9b5c2cd3..14402024 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -40,6 +40,7 @@ type AcmeServiceOptions = { eab?: ClientExternalAccountBindingOptions; skipLocalVerify?: boolean; useMappingProxy?: boolean; + reverseProxy?: string; privateKeyType?: PrivateKeyType; signal?: AbortSignal; }; @@ -91,8 +92,8 @@ export class AcmeService { const urlMapping: UrlMapping = { enabled: false, mappings: { - "acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work", - "dv.acme-v02.api.pki.goog": "google.proxy.handsfree.work", + "acme-v02.api.letsencrypt.org": this.options.reverseProxy || "letsencrypt.proxy.handsfree.work", + "dv.acme-v02.api.pki.goog": this.options.reverseProxy || "google.proxy.handsfree.work", }, }; const conf = await this.getAccountConfig(email, urlMapping); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index 526bd993..9d47d5d8 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -5,6 +5,7 @@ import _ from "lodash-es"; import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js"; import { CertReader } from "./cert-reader.js"; import { CertApplyBasePlugin } from "./base.js"; +import { GoogleClient } from "../../libs/google.js"; export type { CertInfo }; export * from "./cert-reader.js"; @@ -144,9 +145,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin { type: "eab", }, maybeNeed: true, - required: true, + required: false, helper: - "需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials' \n Google:请查看[google获取eab帮助文档](https://github.com/certd/certd/blob/v2/doc/google/google.md)", + "需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'\n Google:请查看[google获取eab帮助文档](https://gitee.com/certd/certd/blob/v2/doc/google/google.md),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱", mergeScript: ` return { show: ctx.compute(({form})=>{ @@ -157,6 +158,26 @@ export class CertApplyPlugin extends CertApplyBasePlugin { }) eabAccessId!: number; + @TaskInput({ + title: "服务账号授权", + component: { + name: "access-selector", + type: "google", + }, + maybeNeed: true, + required: false, + helper: + "google服务账号授权与EAB授权选填其中一个,[服务账号授权获取方法](https://gitee.com/certd/certd/blob/v2/doc/google/google.md)\n服务账号授权需要配置代理或者服务器本身在海外", + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.sslProvider === 'google' + }) + } + `, + }) + googleAccessId!: number; + @TaskInput({ title: "加密算法", value: "rsa_2048", @@ -190,6 +211,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin { }) useProxy = false; + @TaskInput({ + title: "自定义反代地址", + component: { + placeholder: "google.yourproxy.com", + }, + helper: "填写你的自定义反代地址,不要带http://\nletsencrypt反代目标:acme-v02.api.letsencrypt.org\ngoogle反代目标:dv.acme-v02.api.pki.goog", + }) + reverseProxy = ""; + @TaskInput({ title: "跳过本地校验DNS", value: false, @@ -205,9 +235,32 @@ export class CertApplyPlugin extends CertApplyBasePlugin { async onInit() { let eab: any = null; - if (this.eabAccessId) { - eab = await this.ctx.accessService.getById(this.eabAccessId); + + if (this.sslProvider === "google") { + if (this.googleAccessId) { + this.logger.info("您正在使用google服务账号授权"); + const googleAccess = await this.ctx.accessService.getById(this.googleAccessId); + const googleClient = new GoogleClient({ + access: googleAccess, + logger: this.logger, + }); + eab = await googleClient.getEab(); + } else if (this.eabAccessId) { + this.logger.info("您正在使用google EAB授权"); + eab = await this.ctx.accessService.getById(this.eabAccessId); + } else { + this.logger.error("google需要配置EAB授权或服务账号授权"); + return; + } + } else if (this.sslProvider === "zerossl") { + if (this.eabAccessId) { + eab = await this.ctx.accessService.getById(this.eabAccessId); + } else { + this.logger.error("zerossl需要配置EAB授权"); + return; + } } + this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger, @@ -215,6 +268,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { eab, skipLocalVerify: this.skipLocalVerify, useMappingProxy: this.useProxy, + reverseProxy: this.reverseProxy, privateKeyType: this.privateKeyType, // cnameProxyService: this.ctx.cnameProxyService, // dnsProviderCreator: this.createDnsProvider.bind(this), diff --git a/packages/ui/agent/Dockerfile b/packages/ui/agent/Dockerfile new file mode 100644 index 00000000..36f0151c --- /dev/null +++ b/packages/ui/agent/Dockerfile @@ -0,0 +1,7 @@ +FROM greper/certd:latest +ENV certd_agent_enabled=true +CMD ["npm", "run","start"] + + + + diff --git a/packages/ui/certd-client/src/api/modules/api.basic.ts b/packages/ui/certd-client/src/api/modules/api.basic.ts index 741ddc30..71879335 100644 --- a/packages/ui/certd-client/src/api/modules/api.basic.ts +++ b/packages/ui/certd-client/src/api/modules/api.basic.ts @@ -1,4 +1,5 @@ import { request } from "../service"; +import { SiteEnv, SiteInfo } from "/@/store/modules/settings"; export type SysPublicSetting = { registerEnabled: boolean; @@ -24,14 +25,20 @@ export async function getInstallInfo(): Promise { }); } -export async function getSiteInfo(): Promise { +export async function getSiteInfo(): Promise { return await request({ url: "/basic/settings/siteInfo", method: "get" }); } +export async function getSiteEnv(): Promise { + return await request({ + url: "/basic/settings/siteEnv", + method: "get" + }); +} -export async function bindUrl(data): Promise { +export async function bindUrl(data: any): Promise { return await request({ url: "/sys/plus/bindUrl", method: "post", diff --git a/packages/ui/certd-client/src/api/service.ts b/packages/ui/certd-client/src/api/service.ts index 412b35ca..b5285cf2 100644 --- a/packages/ui/certd-client/src/api/service.ts +++ b/packages/ui/certd-client/src/api/service.ts @@ -47,7 +47,13 @@ function createService() { return dataAxios.data; default: // 不是正确的 code - errorCreate(`${dataAxios.msg}: ${response.config.url}`); + const errorMessage = dataAxios.msg; + // @ts-ignore + if (response?.config?.onError) { + // @ts-ignore + response.config.onError(new Error(errorMessage)); + } + errorCreate(`${errorMessage}: ${response.config.url}`); return dataAxios; } } diff --git a/packages/ui/certd-client/src/components/plugins/common/cert-domains-getter.vue b/packages/ui/certd-client/src/components/plugins/common/cert-domains-getter.vue index 0f40a017..1b088c10 100644 --- a/packages/ui/certd-client/src/components/plugins/common/cert-domains-getter.vue +++ b/packages/ui/certd-client/src/components/plugins/common/cert-domains-getter.vue @@ -1,3 +1,8 @@ + + - - diff --git a/packages/ui/certd-client/src/components/plugins/common/output-selector/index.vue b/packages/ui/certd-client/src/components/plugins/common/output-selector/index.vue index 76f770c3..0dacf480 100644 --- a/packages/ui/certd-client/src/components/plugins/common/output-selector/index.vue +++ b/packages/ui/certd-client/src/components/plugins/common/output-selector/index.vue @@ -23,6 +23,7 @@ export default { const pipeline = inject("pipeline") as Ref; const currentStageIndex = inject("currentStageIndex") as Ref; + const currentTaskIndex = inject("currentTaskIndex") as Ref; const currentStepIndex = inject("currentStepIndex") as Ref; const currentTask = inject("currentTask") as Ref; @@ -32,6 +33,7 @@ export default { options.value = pluginGroups.getPreStepOutputOptions({ pipeline: pipeline.value, currentStageIndex: currentStageIndex.value, + currentTaskIndex: currentTaskIndex.value, currentStepIndex: currentStepIndex.value, currentTask: currentTask.value }); diff --git a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue index d2bbd2c7..0b1541eb 100644 --- a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue +++ b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue @@ -1,6 +1,28 @@ + - - diff --git a/packages/ui/certd-client/src/components/plugins/lib/index.ts b/packages/ui/certd-client/src/components/plugins/lib/index.ts index b338d562..abbfb3f6 100644 --- a/packages/ui/certd-client/src/components/plugins/lib/index.ts +++ b/packages/ui/certd-client/src/components/plugins/lib/index.ts @@ -14,7 +14,7 @@ export type RequestHandleReq = { input: T; }; -export async function doRequest(req: RequestHandleReq, opts?: any = {}) { +export async function doRequest(req: RequestHandleReq, opts: any = {}) { const url = req.type === "access" ? "/pi/handle/access" : "/pi/handle/plugin"; const { typeName, action, data, input } = req; const res = await request({ diff --git a/packages/ui/certd-client/src/components/plugins/synology/device-id-getter.vue b/packages/ui/certd-client/src/components/plugins/synology/device-id-getter.vue index c7aca533..e80dc889 100644 --- a/packages/ui/certd-client/src/components/plugins/synology/device-id-getter.vue +++ b/packages/ui/certd-client/src/components/plugins/synology/device-id-getter.vue @@ -1,7 +1,7 @@