diff --git a/README.md b/README.md
index ed568a15..762af031 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,7 @@ make local.run
| [Name.com](https://www.name.com/) | |
| [NameSilo](https://www.namesilo.com/) | |
| [IBM NS1 Connect](https://www.ibm.com/cn-zh/products/ns1-connect/) | |
+| [西部数码](https://www.west.cn/) | |
| [PowerDNS](https://www.powerdns.com/) | |
| ACME 代理 HTTP 请求 | 可申请允许通过 HTTP 请求修改 DNS 的域名 |
diff --git a/README_EN.md b/README_EN.md
index a9f2cf90..fbb97125 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -98,6 +98,7 @@ The following DNS providers are supported:
| [Name.com](https://www.name.com/) | |
| [NameSilo](https://www.namesilo.com/) | |
| [IBM NS1 Connect](https://www.ibm.com/products/ns1-connect/) | |
+| [West.cn](https://www.west.cn/) | |
| [PowerDNS](https://www.powerdns.com/) | |
| ACME Proxy HTTP Request | Supports managing DNS by HTTP request |
diff --git a/go.mod b/go.mod
index e6e4535a..04da51d9 100644
--- a/go.mod
+++ b/go.mod
@@ -82,6 +82,7 @@ require (
github.com/mailru/easyjson v0.9.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
+ github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/qiniu/dyn v1.3.0 // indirect
github.com/qiniu/x v1.10.5 // indirect
diff --git a/go.sum b/go.sum
index efb9e7cf..831e78c0 100644
--- a/go.sum
+++ b/go.sum
@@ -683,6 +683,8 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs=
github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM=
+github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
+github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go
index 248af1b4..41541f01 100644
--- a/internal/applicant/providers.go
+++ b/internal/applicant/providers.go
@@ -19,6 +19,7 @@ import (
providerPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns"
providerTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud"
providerVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine"
+ providerWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
@@ -238,6 +239,22 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
})
return applicant, err
}
+
+ case domain.ApplyDNSProviderTypeWestcn:
+ {
+ access := domain.AccessConfigForWestcn{}
+ if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to decode provider access config: %w", err)
+ }
+
+ applicant, err := providerWestcn.NewChallengeProvider(&providerWestcn.WestcnApplicantConfig{
+ Username: access.Username,
+ ApiPassword: access.ApiPassword,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return applicant, err
+ }
}
return nil, fmt.Errorf("unsupported applicant provider: %s", string(options.Provider))
diff --git a/internal/domain/access.go b/internal/domain/access.go
index dda7d2b4..d045853a 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -140,3 +140,8 @@ type AccessConfigForVolcEngine struct {
type AccessConfigForWebhook struct {
Url string `json:"url"`
}
+
+type AccessConfigForWestcn struct {
+ Username string `json:"username"`
+ ApiPassword string `json:"password"`
+}
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 85b1dd60..32835e6c 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -32,6 +32,7 @@ const (
AccessProviderTypeUCloud = AccessProviderType("ucloud")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
AccessProviderTypeWebhook = AccessProviderType("webhook")
+ AccessProviderTypeWestcn = AccessProviderType("westcn")
)
type ApplyDNSProviderType string
@@ -62,6 +63,7 @@ const (
ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns")
ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS]
ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns")
+ ApplyDNSProviderTypeWestcn = ApplyDNSProviderType("westcn")
)
type DeployProviderType string
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn/westcn.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn/westcn.go
new file mode 100644
index 00000000..f20b5d21
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn/westcn.go
@@ -0,0 +1,39 @@
+package westcn
+
+import (
+ "errors"
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/providers/dns/westcn"
+)
+
+type WestcnApplicantConfig struct {
+ Username string `json:"username"`
+ ApiPassword string `json:"apiPassword"`
+ DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
+ DnsTTL int32 `json:"dnsTTL,omitempty"`
+}
+
+func NewChallengeProvider(config *WestcnApplicantConfig) (challenge.Provider, error) {
+ if config == nil {
+ return nil, errors.New("config is nil")
+ }
+
+ providerConfig := westcn.NewDefaultConfig()
+ providerConfig.Username = config.Username
+ providerConfig.Password = config.ApiPassword
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = int(config.DnsTTL)
+ }
+
+ provider, err := westcn.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/ui/public/imgs/providers/westcn.svg b/ui/public/imgs/providers/westcn.svg
new file mode 100644
index 00000000..9ea5a83c
--- /dev/null
+++ b/ui/public/imgs/providers/westcn.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index afe49ccd..296eec43 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -32,6 +32,7 @@ import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
+import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
type AccessFormFieldValues = Partial>;
type AccessFormPresets = "add" | "edit";
@@ -131,6 +132,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.WEBHOOK:
return ;
+ case ACCESS_PROVIDERS.WESTCN:
+ return ;
}
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
diff --git a/ui/src/components/access/AccessFormWestcnConfig.tsx b/ui/src/components/access/AccessFormWestcnConfig.tsx
new file mode 100644
index 00000000..0b0257ed
--- /dev/null
+++ b/ui/src/components/access/AccessFormWestcnConfig.tsx
@@ -0,0 +1,76 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type AccessConfigForWestcn } from "@/domain/access";
+
+type AccessFormWestcnConfigFieldValues = Nullish;
+
+export type AccessFormWestcnConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormWestcnConfigFieldValues;
+ onValuesChange?: (values: AccessFormWestcnConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormWestcnConfigFieldValues => {
+ return {
+ username: "",
+ apiPassword: "",
+ };
+};
+
+const AccessFormWestcnConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormWestcnConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ username: z
+ .string()
+ .trim()
+ .min(1, t("access.form.westcn_username.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 })),
+ apiPassword: z
+ .string()
+ .min(1, t("access.form.westcn_api_password.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 }))
+ .trim(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormWestcnConfig;
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index 0c0d6986..19167ddc 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -30,6 +30,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForUCloud
| AccessConfigForVolcEngine
| AccessConfigForWebhook
+ | AccessConfigForWestcn
);
usage: AccessUsageType;
}
@@ -150,4 +151,9 @@ export type AccessConfigForVolcEngine = {
export type AccessConfigForWebhook = {
url: string;
};
+
+export type AccessConfigForWestcn = {
+ username: string;
+ apiPassword: string;
+};
// #endregion
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 89d30772..cb8acfb1 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -27,6 +27,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
UCLOUD: "ucloud",
VOLCENGINE: "volcengine",
WEBHOOK: "webhook",
+ WESTCN: "westcn",
} as const);
export type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS];
@@ -69,10 +70,11 @@ export const accessProvidersMap: Map [
@@ -111,6 +113,7 @@ export const APPLY_DNS_PROVIDERS = Object.freeze({
TENCENTCLOUD_DNS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-dns`,
VOLCENGINE: `${ACCESS_PROVIDERS.VOLCENGINE}`, // 兼容旧值,等同于 `VOLCENGINE_DNS`
VOLCENGINE_DNS: `${ACCESS_PROVIDERS.VOLCENGINE}-dns`,
+ WESTCN: `${ACCESS_PROVIDERS.WESTCN}`,
} as const);
export type ApplyDNSProviderType = (typeof APPLY_DNS_PROVIDERS)[keyof typeof APPLY_DNS_PROVIDERS];
@@ -139,6 +142,7 @@ export const applyDNSProvidersMap: Map [
diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json
index 47f59120..36e0a904 100644
--- a/ui/src/i18n/locales/en/nls.access.json
+++ b/ui/src/i18n/locales/en/nls.access.json
@@ -124,6 +124,22 @@
"access.form.qiniu_secret_key.label": "Qiniu SecretKey",
"access.form.qiniu_secret_key.placeholder": "Please enter Qiniu SecretKey",
"access.form.qiniu_secret_key.tooltip": "For more information, see https://portal.qiniu.com/",
+ "access.form.ssh_host.label": "Server host",
+ "access.form.ssh_host.placeholder": "Please enter server host",
+ "access.form.ssh_port.label": "Server port",
+ "access.form.ssh_port.placeholder": "Please enter server port",
+ "access.form.ssh_username.label": "Username",
+ "access.form.ssh_username.placeholder": "Please enter username",
+ "access.form.ssh_password.label": "Password",
+ "access.form.ssh_password.placeholder": "Please enter password",
+ "access.form.ssh_password.tooltip": "Required when using password to connect to SSH.",
+ "access.form.ssh_key.label": "SSH key",
+ "access.form.ssh_key.placeholder": "Please enter SSH key",
+ "access.form.ssh_key.upload": "Choose file ...",
+ "access.form.ssh_key.tooltip": "Required when using key to connect to SSH.",
+ "access.form.ssh_key_passphrase.label": "SSH key passphrase",
+ "access.form.ssh_key_passphrase.placeholder": "Please enter SSH key passphrase",
+ "access.form.ssh_key_passphrase.tooltip": "Optional when using key to connect to SSH.",
"access.form.tencentcloud_secret_id.label": "Tencent Cloud SecretId",
"access.form.tencentcloud_secret_id.placeholder": "Please enter Tencent Cloud SecretId",
"access.form.tencentcloud_secret_id.tooltip": "For more information, see https://cloud.tencent.com/document/product/598/40488?lang=en",
@@ -147,20 +163,10 @@
"access.form.volcengine_secret_access_key.tooltip": "For more information, see https://www.volcengine.com/docs/6291/216571",
"access.form.webhook_url.label": "Webhook URL",
"access.form.webhook_url.placeholder": "Please enter Webhook URL",
- "access.form.ssh_host.label": "Server host",
- "access.form.ssh_host.placeholder": "Please enter server host",
- "access.form.ssh_port.label": "Server port",
- "access.form.ssh_port.placeholder": "Please enter server port",
- "access.form.ssh_username.label": "Username",
- "access.form.ssh_username.placeholder": "Please enter username",
- "access.form.ssh_password.label": "Password",
- "access.form.ssh_password.placeholder": "Please enter password",
- "access.form.ssh_password.tooltip": "Required when using password to connect to SSH.",
- "access.form.ssh_key.label": "SSH key",
- "access.form.ssh_key.placeholder": "Please enter SSH key",
- "access.form.ssh_key.upload": "Choose file ...",
- "access.form.ssh_key.tooltip": "Required when using key to connect to SSH.",
- "access.form.ssh_key_passphrase.label": "SSH key passphrase",
- "access.form.ssh_key_passphrase.placeholder": "Please enter SSH key passphrase",
- "access.form.ssh_key_passphrase.tooltip": "Optional when using key to connect to SSH."
+ "access.form.westcn_username.label": "West.cn username",
+ "access.form.westcn_username.placeholder": "Please enter West.cn username",
+ "access.form.westcn_username.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html",
+ "access.form.westcn_api_password.label": "West.cn API password",
+ "access.form.westcn_api_password.placeholder": "Please enter West.cn API password",
+ "access.form.westcn_api_password.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html"
}
diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json
index 17764565..3bed9469 100644
--- a/ui/src/i18n/locales/en/nls.common.json
+++ b/ui/src/i18n/locales/en/nls.common.json
@@ -93,6 +93,7 @@
"common.provider.volcengine.live": "Volcengine - Live",
"common.provider.volcengine.tos": "Volcengine - Tinder Object Storage (TOS)",
"common.provider.webhook": "Webhook",
+ "common.provider.westcn": "West.cn",
"common.notifier.bark": "Bark",
"common.notifier.dingtalk": "DingTalk",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index 35a10eda..d3baaf67 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -124,6 +124,22 @@
"access.form.qiniu_secret_key.label": "七牛云 SecretKey",
"access.form.qiniu_secret_key.placeholder": "请输入七牛云 SecretKey",
"access.form.qiniu_secret_key.tooltip": "这是什么?请参阅 https://portal.qiniu.com/",
+ "access.form.ssh_host.label": "服务器地址",
+ "access.form.ssh_host.placeholder": "请输入服务器地址",
+ "access.form.ssh_port.label": "服务器端口",
+ "access.form.ssh_port.placeholder": "请输入服务器端口",
+ "access.form.ssh_username.label": "用户名",
+ "access.form.ssh_username.placeholder": "请输入用户名",
+ "access.form.ssh_password.label": "密码",
+ "access.form.ssh_password.placeholder": "请输入密码",
+ "access.form.ssh_password.tooltip": "使用密码连接到 SSH 时必填。
该字段与密钥文件字段二选一。",
+ "access.form.ssh_key.label": "SSH 密钥",
+ "access.form.ssh_key.placeholder": "请输入 SSH 密钥文件",
+ "access.form.ssh_key.upload": "选择文件",
+ "access.form.ssh_key.tooltip": "使用 SSH 密钥连接到 SSH 时必填。
该字段与密码字段二选一。",
+ "access.form.ssh_key_passphrase.label": "SSH 密钥口令",
+ "access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令",
+ "access.form.ssh_key_passphrase.tooltip": "使用 SSH 密钥连接到 SSH 时选填。",
"access.form.tencentcloud_secret_id.label": "腾讯云 SecretId",
"access.form.tencentcloud_secret_id.placeholder": "请输入腾讯云 SecretId",
"access.form.tencentcloud_secret_id.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/598/40488",
@@ -147,20 +163,10 @@
"access.form.volcengine_secret_access_key.tooltip": "这是什么?请参阅 https://www.volcengine.com/docs/6291/216571",
"access.form.webhook_url.label": "Webhook 回调地址",
"access.form.webhook_url.placeholder": "请输入 Webhook 回调地址",
- "access.form.ssh_host.label": "服务器地址",
- "access.form.ssh_host.placeholder": "请输入服务器地址",
- "access.form.ssh_port.label": "服务器端口",
- "access.form.ssh_port.placeholder": "请输入服务器端口",
- "access.form.ssh_username.label": "用户名",
- "access.form.ssh_username.placeholder": "请输入用户名",
- "access.form.ssh_password.label": "密码",
- "access.form.ssh_password.placeholder": "请输入密码",
- "access.form.ssh_password.tooltip": "使用密码连接到 SSH 时必填。
该字段与密钥文件字段二选一。",
- "access.form.ssh_key.label": "SSH 密钥",
- "access.form.ssh_key.placeholder": "请输入 SSH 密钥文件",
- "access.form.ssh_key.upload": "选择文件",
- "access.form.ssh_key.tooltip": "使用 SSH 密钥连接到 SSH 时必填。
该字段与密码字段二选一。",
- "access.form.ssh_key_passphrase.label": "SSH 密钥口令",
- "access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令",
- "access.form.ssh_key_passphrase.tooltip": "使用 SSH 密钥连接到 SSH 时选填。"
+ "access.form.westcn_username.label": "西部数码用户名",
+ "access.form.westcn_username.placeholder": "请输入西部数码用户名",
+ "access.form.westcn_username.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html",
+ "access.form.westcn_api_password.label": "西部数据 API 密码",
+ "access.form.westcn_api_password.placeholder": "请输入西部数据 API 密码",
+ "access.form.westcn_api_password.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html"
}
diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json
index feca7718..d555d473 100644
--- a/ui/src/i18n/locales/zh/nls.common.json
+++ b/ui/src/i18n/locales/zh/nls.common.json
@@ -93,6 +93,7 @@
"common.provider.volcengine.live": "火山引擎 - 视频直播 Live",
"common.provider.volcengine.tos": "火山引擎 - 对象存储 TOS",
"common.provider.webhook": "Webhook",
+ "common.provider.westcn": "西部数码",
"common.notifier.bark": "Bark",
"common.notifier.dingtalk": "钉钉",