feat(provider): new acme dns-01 provider: xinnet

This commit is contained in:
Fu Diwei
2025-11-15 20:23:05 +08:00
parent 69af915147
commit 614d4770e0
19 changed files with 589 additions and 25 deletions
@@ -0,0 +1,28 @@
package applicators
import (
"fmt"
"github.com/go-acme/lego/v4/challenge"
"github.com/certimate-go/certimate/internal/domain"
xinnet "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/xinnet"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeXinnet, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
credentials := domain.AccessConfigForXinnet{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := xinnet.NewChallengeProvider(&xinnet.ChallengeProviderConfig{
AgentId: credentials.AgentId,
ApiPassword: credentials.ApiPassword,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return provider, err
})
}
+5
View File
@@ -528,6 +528,11 @@ type AccessConfigForWestcn struct {
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForXinnet struct {
AgentId string `json:"agentId"`
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForZeroSSL struct {
AccessConfigForACMEExternalAccountBinding
}
+2
View File
@@ -108,6 +108,7 @@ const (
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWeComBot = AccessProviderType("wecombot")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeXinnet = AccessProviderType("xinnet")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
)
@@ -209,6 +210,7 @@ const (
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
ACMEDns01ProviderTypeVultr = ACMEDns01ProviderType(AccessProviderTypeVultr)
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
ACMEDns01ProviderTypeXinnet = ACMEDns01ProviderType(AccessProviderTypeXinnet)
)
type ACMEHttp01ProviderType string
@@ -0,0 +1,39 @@
package xinnet
import (
"errors"
"time"
"github.com/certimate-go/certimate/pkg/core"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal"
)
type ChallengeProviderConfig struct {
AgentId string `json:"agentId"`
ApiPassword string `json:"apiPassword"`
DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
if config == nil {
return nil, errors.New("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.AgentID = config.AgentId
providerConfig.AppSecret = config.ApiPassword
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}
@@ -0,0 +1,149 @@
package internal
import (
"errors"
"fmt"
"sync"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/samber/lo"
xinnetsdk "github.com/certimate-go/certimate/pkg/sdk3rd/xinnet"
)
const (
envNamespace = "DNSLA_"
EnvAgentId = envNamespace + "AGENT_ID"
EnvAppSecret = envNamespace + "APP_SECRET"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AgentID string
AppSecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *xinnetsdk.Client
config *Config
recordIDs map[string]*int64
recordIDsMu sync.Mutex
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAgentId, EnvAppSecret)
if err != nil {
return nil, fmt.Errorf("xinnet: %w", err)
}
config := NewDefaultConfig()
config.AgentID = values[EnvAgentId]
config.AppSecret = values[EnvAppSecret]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("xinnet: the configuration of the DNS provider is nil")
}
client, err := xinnetsdk.NewClient(config.AgentID, config.AppSecret)
if err != nil {
return nil, err
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
client: client,
config: config,
recordIDs: make(map[string]*int64),
recordIDsMu: sync.Mutex{},
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err)
}
// REF: https://apidoc.xin.cn/doc-7283900
xinnetDnsCreateReq := &xinnetsdk.DnsCreateRequest{
DomainName: lo.ToPtr(dns01.UnFqdn(authZone)),
RecordName: lo.ToPtr(dns01.UnFqdn(info.EffectiveFQDN)),
Type: lo.ToPtr("TXT"),
Value: lo.ToPtr(info.Value),
Line: lo.ToPtr("默认"),
Ttl: lo.ToPtr(int32(d.config.TTL)),
}
xinnetDnsCreateResp, err := d.client.DnsCreate(xinnetDnsCreateReq)
if err != nil {
return fmt.Errorf("xinnet: error when create record: %w", err)
}
d.recordIDsMu.Lock()
d.recordIDs[token] = xinnetDnsCreateResp.Data
d.recordIDsMu.Unlock()
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err)
}
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("xinnet: unknown record ID for '%s'", info.EffectiveFQDN)
}
// REF: https://apidoc.xin.cn/doc-7283901
xinnetDnsDeleteReq := &xinnetsdk.DnsDeleteRequest{
DomainName: lo.ToPtr(dns01.UnFqdn(authZone)),
RecordId: recordID,
}
if _, err := d.client.DnsDelete(xinnetDnsDeleteReq); err != nil {
return fmt.Errorf("xinnet: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
+42
View File
@@ -0,0 +1,42 @@
package xinnet
import (
"context"
)
type DnsCreateRequest struct {
DomainName *string `json:"domainName,omitempty"`
RecordName *string `json:"recordName,omitempty"`
Type *string `json:"type,omitempty"`
Value *string `json:"value,omitempty"`
Line *string `json:"line,omitempty"`
Ttl *int32 `json:"ttl,omitempty"`
Mx *int32 `json:"mx,omitempty"`
Status *int32 `json:"status,omitempty"`
}
type DnsCreateResponse struct {
apiResponseBase
Data *int64 `json:"data,omitempty"`
}
func (c *Client) DnsCreate(req *DnsCreateRequest) (*DnsCreateResponse, error) {
return c.DnsCreateWithContext(context.Background(), req)
}
func (c *Client) DnsCreateWithContext(ctx context.Context, req *DnsCreateRequest) (*DnsCreateResponse, error) {
httpreq, err := c.newRequest("/dns/create/")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &DnsCreateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+35
View File
@@ -0,0 +1,35 @@
package xinnet
import (
"context"
)
type DnsDeleteRequest struct {
DomainName *string `json:"domainName,omitempty"`
RecordId *int64 `json:"recordId,omitempty"`
}
type DnsDeleteResponse struct {
apiResponseBase
}
func (c *Client) DnsDelete(req *DnsDeleteRequest) (*DnsDeleteResponse, error) {
return c.DnsDeleteWithContext(context.Background(), req)
}
func (c *Client) DnsDeleteWithContext(ctx context.Context, req *DnsDeleteRequest) (*DnsDeleteResponse, error) {
httpreq, err := c.newRequest("/dns/delete/")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &DnsDeleteResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+149
View File
@@ -0,0 +1,149 @@
package xinnet
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
client *resty.Client
}
func NewClient(agentId, appSecret string) (*Client, error) {
if agentId == "" {
return nil, fmt.Errorf("sdkerr: unset agentId")
}
if appSecret == "" {
return nil, fmt.Errorf("sdkerr: unset appSecret")
}
client := resty.New().
SetBaseURL("https://apiv2.xinnet.com/api").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// 生成时间戳
timestamp := time.Now().UTC().Format("20060102T150405Z")
// 获取请求路径,注意结尾必须是 "/"
urlPath := "/"
if req.URL != nil {
urlPath = req.URL.Path
if !strings.HasSuffix(urlPath, "/") {
urlPath += "/"
}
}
// 获取请求方法
requestMethod := req.Method
// 获取请求体
requestBody := ""
if req.Body != nil {
reader, err := req.GetBody()
if err != nil {
return err
}
defer reader.Close()
payloadb, err := io.ReadAll(reader)
if err != nil {
return err
}
requestBody = string(payloadb)
}
// 计算签名
algorithm := "HMAC-SHA256"
stringToSign := algorithm + "\n" +
timestamp + "\n" +
requestMethod + "\n" +
urlPath + "\n" +
requestBody
key := []byte(appSecret)
h := hmac.New(sha256.New, key)
h.Write([]byte(stringToSign))
signature := hex.EncodeToString(h.Sum(nil))
// 设置请求头
req.Header.Set("timestamp", timestamp)
req.Header.Set("authorization", fmt.Sprintf("%s Access=%s, Signature=%s", algorithm, agentId, signature))
return nil
})
return &Client{client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(path string) (*resty.Request, error) {
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
req := c.client.R()
req.Method = http.MethodPost
req.URL = path
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
// WARN:
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res apiResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
} else {
if tcode := res.GetCode(); tcode != "0" {
return resp, fmt.Errorf("sdkerr: code='%s', msg='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
+39
View File
@@ -0,0 +1,39 @@
package xinnet
type apiResponse interface {
GetCode() string
GetMessage() string
GetRequestId() string
}
type apiResponseBase struct {
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
RequestId *string `json:"requestId,omitempty"`
}
func (r *apiResponseBase) GetCode() string {
if r.Code == nil {
return ""
}
return *r.Code
}
func (r *apiResponseBase) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
func (r *apiResponseBase) GetRequestId() string {
if r.RequestId == nil {
return ""
}
return *r.RequestId
}
var _ apiResponse = (*apiResponseBase)(nil)
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@@ -95,6 +95,7 @@ import AccessConfigFieldsProviderWangsu from "./AccessConfigFieldsProviderWangsu
import AccessConfigFieldsProviderWebhook from "./AccessConfigFieldsProviderWebhook";
import AccessConfigFieldsProviderWeComBot from "./AccessConfigFieldsProviderWeComBot";
import AccessConfigFieldsProviderWestcn from "./AccessConfigFieldsProviderWestcn";
import AccessConfigFieldsProviderXinnet from "./AccessConfigFieldsProviderXinnet";
import AccessConfigFieldsProviderZeroSSL from "./AccessConfigFieldsProviderZeroSSL";
const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentType<any>>> = {
@@ -195,6 +196,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.WEBHOOK]: AccessConfigFieldsProviderWebhook,
[ACCESS_PROVIDERS.WECOMBOT]: AccessConfigFieldsProviderWeComBot,
[ACCESS_PROVIDERS.WESTCN]: AccessConfigFieldsProviderWestcn,
[ACCESS_PROVIDERS.XINNET]: AccessConfigFieldsProviderXinnet,
[ACCESS_PROVIDERS.ZEROSSL]: AccessConfigFieldsProviderZeroSSL,
};
@@ -3,6 +3,8 @@ import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Tips from "@/components/Tips";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProvider35cn = () => {
@@ -17,13 +19,7 @@ const AccessConfigFormFieldsProvider35cn = () => {
return (
<>
<Form.Item
name={[parentNamePath, "username"]}
initialValue={initialValues.username}
label={t("access.form.35cn_username.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.35cn_username.tooltip") }}></span>}
>
<Form.Item name={[parentNamePath, "username"]} initialValue={initialValues.username} label={t("access.form.35cn_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.35cn_username.placeholder")} />
</Form.Item>
@@ -32,10 +28,13 @@ const AccessConfigFormFieldsProvider35cn = () => {
initialValue={initialValues.apiPassword}
label={t("access.form.35cn_api_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.35cn_api_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.35cn_api_password.placeholder")} />
</Form.Item>
<Form.Item>
<Tips message={<span dangerouslySetInnerHTML={{ __html: t("access.form.35cn_agent.guide") }}></span>} />
</Form.Item>
</>
);
};
@@ -3,6 +3,8 @@ import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Tips from "@/components/Tips";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderWestcn = () => {
@@ -17,13 +19,7 @@ const AccessConfigFormFieldsProviderWestcn = () => {
return (
<>
<Form.Item
name={[parentNamePath, "username"]}
initialValue={initialValues.username}
label={t("access.form.westcn_username.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.westcn_username.tooltip") }}></span>}
>
<Form.Item name={[parentNamePath, "username"]} initialValue={initialValues.username} label={t("access.form.westcn_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.westcn_username.placeholder")} />
</Form.Item>
@@ -32,10 +28,13 @@ const AccessConfigFormFieldsProviderWestcn = () => {
initialValue={initialValues.apiPassword}
label={t("access.form.westcn_api_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.westcn_api_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.westcn_api_password.placeholder")} />
</Form.Item>
<Form.Item>
<Tips message={<span dangerouslySetInnerHTML={{ __html: t("access.form.westcn_agent.guide") }}></span>} />
</Form.Item>
</>
);
};
@@ -0,0 +1,63 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Tips from "@/components/Tips";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderXinnet = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item name={[parentNamePath, "agentId"]} initialValue={initialValues.agentId} label={t("access.form.xinnet_agent_id.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.xinnet_agent_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "apiPassword"]}
initialValue={initialValues.apiPassword}
label={t("access.form.xinnet_api_password.label")}
rules={[formRule]}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.xinnet_api_password.placeholder")} />
</Form.Item>
<Form.Item>
<Tips message={<span dangerouslySetInnerHTML={{ __html: t("access.form.xinnet_agent.guide") }}></span>} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
agentId: "",
apiPassword: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
agentId: z.string().nonempty(t("access.form.xinnet_agent_id.placeholder")),
apiPassword: z.string().nonempty(t("access.form.xinnet_api_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderXinnet, {
getInitialValues,
getSchema,
});
export default _default;
+4
View File
@@ -111,6 +111,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
WEBHOOK: "webhook",
WECOMBOT: "wecombot",
WESTCN: "westcn",
XINNET: "xinnet",
ZEROSSL: "zerossl",
} as const);
@@ -214,6 +215,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS["35CN"], "provider.35cn", "/imgs/providers/35cn.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.XINNET, "provider.xinnet", "/imgs/providers/xinnet.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.TECHNITIUMDNS, "provider.technitiumdns", "/imgs/providers/technitiumdns.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.RFC2136, "provider.rfc2136", "/imgs/providers/rfc.png", [ACCESS_USAGES.DNS]],
@@ -376,6 +378,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
VOLCENGINE_DNS: `${ACCESS_PROVIDERS.VOLCENGINE}-dns`,
VULTR: `${ACCESS_PROVIDERS.VULTR}`,
WESTCN: `${ACCESS_PROVIDERS.WESTCN}`,
XINNET: `${ACCESS_PROVIDERS.XINNET}`,
} as const);
export type ACMEDns01ProviderType = (typeof ACME_DNS01_PROVIDERS)[keyof typeof ACME_DNS01_PROVIDERS];
@@ -440,6 +443,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.UCLOUD_UDNR, "provider.ucloud.udnr"],
[ACME_DNS01_PROVIDERS.WESTCN, "provider.westcn"],
[ACME_DNS01_PROVIDERS["35CN"], "provider.35cn"],
[ACME_DNS01_PROVIDERS.XINNET, "provider.xinnet"],
[ACME_DNS01_PROVIDERS.POWERDNS, "provider.powerdns"],
[ACME_DNS01_PROVIDERS.TECHNITIUMDNS, "provider.technitiumdns"],
[ACME_DNS01_PROVIDERS.RFC2136, "provider.rfc2136"],
+8 -5
View File
@@ -60,10 +60,9 @@
"access.form.1panel_api_key.tooltip": "For more information, see <a href=\"https://docs.1panel.pro/dev_manual/api_manual/\" target=\"_blank\">https://docs.1panel.pro/dev_manual/api_manual/</a>",
"access.form.35cn_username.label": "35.cn agent username",
"access.form.35cn_username.placeholder": "Please enter West.cn agent username",
"access.form.35cn_username.tooltip": "For more information, see <a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.35cn_api_password.label": "35.cn agent API password",
"access.form.35cn_api_password.placeholder": "Please enter West.cn agent API password",
"access.form.35cn_api_password.tooltip": "For more information, see <a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.35cn_agent.guide": "West.cn API only supports calls from agents. Learn more about this: <br><a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.acmeca_endpoint.label": "Endpoint",
"access.form.acmeca_endpoint.placeholder": "Please enter endpoint",
"access.form.acmeca_endpoint.tooltip": "For more information, see <a href=\"https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1\" target=\"_blank\">https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1</a>",
@@ -632,12 +631,16 @@
"access.form.webhook_preset_data.option.common.label": "General template",
"access.form.wecombot_webhook_url.label": "WeCom bot Webhook URL",
"access.form.wecombot_webhook_url.placeholder": "Please enter WeCom bot Webhook URL",
"access.form.wecombot_webhook_url.tooltip": "For more information, see <a href=\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\" target=\"_blank\">https://open.work.weixin.qq.com/help2/pc/18401</a>",
"access.form.wecombot_webhook_url.tooltip": "For more information, see <a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.westcn_username.label": "West.cn agent username",
"access.form.westcn_username.placeholder": "Please enter West.cn agent username",
"access.form.westcn_username.tooltip": "For more information, see <a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.westcn_api_password.label": "West.cn agent API password",
"access.form.westcn_api_password.placeholder": "Please enter West.cn agent API password",
"access.form.westcn_api_password.tooltip": "For more information, see <a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.westcn_agent.guide": "West.cn API only supports calls from agents. Learn more about this: <br><a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.xinnet_agent_id.label": "Xinnet agent ID",
"access.form.xinnet_agent_id.placeholder": "Please enter Xinnet agent ID",
"access.form.xinnet_api_password.label": "Xinnet agent API password",
"access.form.xinnet_api_password.placeholder": "Please enter Xinnet agent API password",
"access.form.xinnet_agent.guide": "Xinnet API only supports calls from agents. Learn more about this: <br><a href=\"https://apidoc.xin.cn/doc-7283837\" target=\"_blank\">https://apidoc.xin.cn/doc-7283837</a>",
"access.form.zerossl_eab.guide": "Learn more about using EAB key in ZeroSSL: <br><a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>"
}
+1
View File
@@ -200,6 +200,7 @@
"provider.webhook": "Webhook",
"provider.wecombot": "WeCom Bot",
"provider.westcn": "West.cn",
"provider.xinnet": "Xinnet",
"provider.zerossl": "ZeroSSL",
"provider.category.all": "All",
+7 -4
View File
@@ -59,10 +59,9 @@
"access.form.1panel_api_key.tooltip": "这是什么?请参阅 <a href=\"https://1panel.cn/docs/dev_manual/api_manual/\" target=\"_blank\">https://1panel.cn/docs/dev_manual/api_manual/</a>",
"access.form.35cn_username.label": "三五互联代理商用户名",
"access.form.35cn_username.placeholder": "请输入三五互联代理商用户名",
"access.form.35cn_username.tooltip": "这是什么?请参阅 <a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.35cn_api_password.label": "三五互联代理商 API 密码",
"access.form.35cn_api_password.placeholder": "请输入三五互联代理商 API 密码",
"access.form.35cn_api_password.tooltip": "这是什么?请参阅 <a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.35cn_agent.guide": "三五互联 API 仅支持代理商调用。点击下方链接了解更多:<br><a href=\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\" target=\"_blank\">https://www.35.com/agent/mode-api.asp</a>",
"access.form.acmeca_endpoint.label": "服务端点",
"access.form.acmeca_endpoint.placeholder": "请输入服务端点",
"access.form.acmeca_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1\" target=\"_blank\">https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1</a>",
@@ -634,9 +633,13 @@
"access.form.wecombot_webhook_url.tooltip": "这是什么?请参阅 <a href=\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\" target=\"_blank\">https://open.work.weixin.qq.com/help2/pc/18401</a>",
"access.form.westcn_username.label": "西部数码代理商用户名",
"access.form.westcn_username.placeholder": "请输入西部数码代理商用户名",
"access.form.westcn_username.tooltip": "这是什么?请参阅 <a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.westcn_api_password.label": "西部数码代理商 API 密码",
"access.form.westcn_api_password.placeholder": "请输入西部数码代理商 API 密码",
"access.form.westcn_api_password.tooltip": "这是什么?请参阅 <a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.westcn_agent.guide": "西部数码 API 仅支持代理商调用。点击下方链接了解更多:<br><a href=\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\" target=\"_blank\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>",
"access.form.xinnet_agent_id.label": "新网数码代理商编号",
"access.form.xinnet_agent_id.placeholder": "请输入新网数码代理商编号",
"access.form.xinnet_api_password.label": "新网数码代理商 API 密码",
"access.form.xinnet_api_password.placeholder": "请输入新网数码代理商 API 密码",
"access.form.xinnet_agent.guide": "新网数码 API 仅支持代理商调用。点击下方链接了解更多:<br><a href=\"https://apidoc.xin.cn/doc-7283837\" target=\"_blank\">https://apidoc.xin.cn/doc-7283837</a>",
"access.form.zerossl_eab.guide": "点击下方链接了解如何获取 ZeroSSL EAB:<br><a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>"
}
+2
View File
@@ -200,7 +200,9 @@
"provider.webhook": "Webhook",
"provider.wecombot": "企业微信群机器人",
"provider.westcn": "西部数码",
"provider.xinnet": "新网数码",
"provider.zerossl": "ZeroSSL",
"provider.category.all": "全部",
"provider.category.cdn": "CDN",
"provider.category.storage": "文件存储",