diff --git a/backend/internal/cert/apply/apply.go b/backend/internal/cert/apply/apply.go
index 3bfd179..f340141 100644
--- a/backend/internal/cert/apply/apply.go
+++ b/backend/internal/cert/apply/apply.go
@@ -3,6 +3,7 @@ package apply
import (
"ALLinSSL/backend/internal/access"
"ALLinSSL/backend/internal/cert"
+ "ALLinSSL/backend/internal/cert/apply/lego/acmedns"
"ALLinSSL/backend/internal/cert/apply/lego/bt"
"ALLinSSL/backend/internal/cert/apply/lego/jdcloud"
"ALLinSSL/backend/internal/cert/apply/lego/webhook"
@@ -242,6 +243,12 @@ func GetDNSProvider(providerName string, creds map[string]string, httpClient *ht
config.SecretKey = creds["secret_key"]
config.PropagationTimeout = maxWait
return edgeone.NewDNSProviderConfig(config)
+ case "acmedns":
+ config := &acmedns.Config{
+ ServerURL: creds["server_url"],
+ Credentials: creds["credentials"],
+ }
+ return acmedns.NewDNSProviderConfig(config)
default:
return nil, fmt.Errorf("不支持的 DNS Provider: %s", providerName)
diff --git a/backend/internal/cert/apply/lego/acmedns/lego.go b/backend/internal/cert/apply/lego/acmedns/lego.go
new file mode 100644
index 0000000..de359a8
--- /dev/null
+++ b/backend/internal/cert/apply/lego/acmedns/lego.go
@@ -0,0 +1,125 @@
+package acmedns
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ legoacmedns "github.com/go-acme/lego/v4/providers/dns/acmedns"
+ "github.com/nrdcg/goacmedns"
+)
+
+type Config struct {
+ ServerURL string `json:"server_url"`
+ Credentials string `json:"credentials"`
+}
+
+type acmeDNSUpdater interface {
+ UpdateTXTRecord(ctx context.Context, account goacmedns.Account, value string) error
+}
+
+type singleAccountProvider struct {
+ account goacmedns.Account
+ client acmeDNSUpdater
+}
+
+var _ challenge.Provider = (*singleAccountProvider)(nil)
+
+func NewDNSProviderConfig(config *Config) (challenge.Provider, error) {
+ if config == nil {
+ return nil, errors.New("acme-dns: the configuration of the DNS provider is nil")
+ }
+
+ serverURL := strings.TrimSpace(config.ServerURL)
+ if serverURL == "" {
+ return nil, errors.New("acme-dns: server_url is required")
+ }
+
+ credentials := strings.TrimSpace(config.Credentials)
+ if credentials == "" {
+ return nil, errors.New("acme-dns: credentials is required")
+ }
+
+ if isAccountMappingCredentials(credentials) {
+ return newStorageBackedProvider(serverURL, credentials)
+ }
+
+ account, err := parseSingleAccountCredentials(credentials)
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := goacmedns.NewClient(serverURL)
+ if err != nil {
+ return nil, err
+ }
+
+ account.ServerURL = serverURL
+
+ return &singleAccountProvider{
+ account: account,
+ client: client,
+ }, nil
+}
+
+func createTempCredentialsFile(credentials string) (string, error) {
+ tempFile, err := os.CreateTemp("", "allinssl-acmedns-*.json")
+ if err != nil {
+ return "", fmt.Errorf("acme-dns: failed to create temp credentials file: %w", err)
+ }
+ defer tempFile.Close()
+
+ if _, err := tempFile.WriteString(credentials); err != nil {
+ return "", fmt.Errorf("acme-dns: failed to write temp credentials file: %w", err)
+ }
+
+ return tempFile.Name(), nil
+}
+
+func (p *singleAccountProvider) Present(domain, _, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ return p.client.UpdateTXTRecord(context.Background(), p.account, info.Value)
+}
+
+func (p *singleAccountProvider) CleanUp(_, _, _ string) error {
+ return nil
+}
+
+func isAccountMappingCredentials(credentials string) bool {
+ var accounts map[string]goacmedns.Account
+
+ return json.Unmarshal([]byte(credentials), &accounts) == nil
+}
+
+func parseSingleAccountCredentials(credentials string) (goacmedns.Account, error) {
+ var account goacmedns.Account
+
+ if err := json.Unmarshal([]byte(credentials), &account); err != nil {
+ return goacmedns.Account{}, fmt.Errorf("acme-dns: credentials must be either a /register account JSON or a map[domain]account JSON: %w", err)
+ }
+
+ if strings.TrimSpace(account.Username) == "" || strings.TrimSpace(account.Password) == "" || strings.TrimSpace(account.SubDomain) == "" {
+ return goacmedns.Account{}, errors.New("acme-dns: single-account credentials require username, password, and subdomain")
+ }
+
+ return account, nil
+}
+
+func newStorageBackedProvider(serverURL, credentials string) (challenge.Provider, error) {
+ tempFilePath, err := createTempCredentialsFile(credentials)
+ if err != nil {
+ return nil, err
+ }
+
+ providerConfig := legoacmedns.NewDefaultConfig()
+ providerConfig.APIBase = serverURL
+ providerConfig.StoragePath = tempFilePath
+
+ return legoacmedns.NewDNSProviderConfig(providerConfig)
+}
diff --git a/backend/migrations/init.go b/backend/migrations/init.go
index 27f0ee4..7a83b78 100644
--- a/backend/migrations/init.go
+++ b/backend/migrations/init.go
@@ -201,6 +201,7 @@ func init() {
InsertIfNotExists(db, "access_type", map[string]any{"name": "btdomain", "type": "dns"}, []string{"name", "type"}, []any{"btdomain", "dns"})
InsertIfNotExists(db, "access_type", map[string]any{"name": "edgeone", "type": "dns"}, []string{"name", "type"}, []any{"edgeone", "dns"})
+ InsertIfNotExists(db, "access_type", map[string]any{"name": "acmedns", "type": "dns"}, []string{"name", "type"}, []any{"acmedns", "dns"})
err = sqlite_migrate.EnsureDatabaseWithTables(
"data/site_monitor.db",
diff --git a/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmedns.svg b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmedns.svg
new file mode 100644
index 0000000..6c5eaee
--- /dev/null
+++ b/frontend/apps/allin-ssl/src/assets/icons/svg/resources/acmedns.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/apps/allin-ssl/src/config/data.tsx b/frontend/apps/allin-ssl/src/config/data.tsx
index e6a3048..be18e6e 100644
--- a/frontend/apps/allin-ssl/src/config/data.tsx
+++ b/frontend/apps/allin-ssl/src/config/data.tsx
@@ -297,6 +297,12 @@ export const ApiProjectConfig: Record = {
},
sort: 38,
},
+ acmedns: {
+ name: "ACME DNS",
+ icon: "acmedns",
+ type: ["dns"],
+ sort: 39,
+ },
plugin: {
name: "插件",
icon: "plugin",
diff --git a/frontend/apps/allin-ssl/src/types/access.d.ts b/frontend/apps/allin-ssl/src/types/access.d.ts
index 4676a26..a86edd9 100644
--- a/frontend/apps/allin-ssl/src/types/access.d.ts
+++ b/frontend/apps/allin-ssl/src/types/access.d.ts
@@ -64,6 +64,7 @@ export interface AddAccessParams<
| LecdnAccessConfig
| ConstellixAccessConfig
| WebhookAccessConfig
+ | AcmeDnsAccessConfig
| SpaceshipAccessConfig
| BTDomainAccessConfig
> {
@@ -102,6 +103,7 @@ export interface UpdateAccessParams<
| LecdnAccessConfig
| ConstellixAccessConfig
| WebhookAccessConfig
+ | AcmeDnsAccessConfig
| SpaceshipAccessConfig
| BTDomainAccessConfig
> extends AddAccessParams {
@@ -320,6 +322,11 @@ export interface WebhookAccessConfig {
ignore_ssl: "0" | "1";
}
+export interface AcmeDnsAccessConfig {
+ server_url: string;
+ credentials: string;
+}
+
export interface SpaceshipAccessConfig {
api_key: string;
api_secret: string;
diff --git a/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx b/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx
index 7a9253f..d666ebb 100644
--- a/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx
+++ b/frontend/apps/allin-ssl/src/views/authApiManage/useController.tsx
@@ -55,6 +55,7 @@ import type {
LecdnAccessConfig,
ConstellixAccessConfig,
WebhookAccessConfig,
+ AcmeDnsAccessConfig,
SpaceshipAccessConfig,
BTDomainAccessConfig,
} from "@/types/access";
@@ -554,6 +555,39 @@ export const useApiFormController = (
callback();
},
},
+ server_url: {
+ required: true,
+ trigger: "input",
+ validator: (
+ rule: FormItemRule,
+ value: string,
+ callback: (error?: Error) => void
+ ) => {
+ if (!isUrl(value)) {
+ return callback(new Error("请输入正确的 Server URL"));
+ }
+ callback();
+ },
+ },
+ credentials: {
+ required: true,
+ trigger: "input",
+ validator: (
+ rule: FormItemRule,
+ value: string,
+ callback: (error?: Error) => void
+ ) => {
+ if (!value || value.trim() === "") {
+ return callback(new Error("请输入 Credentials JSON"));
+ }
+ try {
+ JSON.parse(value);
+ } catch (error) {
+ return callback(new Error("Credentials 必须是合法 JSON"));
+ }
+ callback();
+ },
+ },
api_key: {
trigger: "input",
validator: (
@@ -1320,6 +1354,45 @@ export const useApiFormController = (
})
);
break;
+ case "acmedns":
+ items.push(
+ useFormInput("Server URL", "config.server_url", {
+ allowInput: noSideSpace,
+ placeholder: "https://auth.acme-dns.io",
+ }),
+ useFormTextarea(
+ "Credentials(JSON)",
+ "config.credentials",
+ {
+ rows: 8,
+ placeholder:
+ '{\n "username": "xxx",\n "password": "xxx",\n "subdomain": "xxx",\n "fulldomain": "xxx.auth.acme-dns.io"\n}',
+ }
+ ),
+ useFormCustom(() => {
+ return (
+
+
+ Credentials 需要填写 acme-dns /register{" "}
+ 接口返回的单条 JSON。
+
+ 官方项目地址
+
+
+
+ );
+ })
+ );
+ break;
case "spaceship":
items.push(
useFormInput("API Key", "config.api_key", {
@@ -1728,6 +1801,12 @@ export const useApiFormController = (
ignore_ssl: "0",
} as WebhookAccessConfig;
break;
+ case "acmedns":
+ param.value.config = {
+ server_url: "https://auth.acme-dns.io",
+ credentials: "",
+ } as AcmeDnsAccessConfig;
+ break;
case "spaceship":
param.value.config = {
api_key: "",
diff --git a/go.mod b/go.mod
index 76427cd..43db1f6 100644
--- a/go.mod
+++ b/go.mod
@@ -119,6 +119,7 @@ require (
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/bunny-go v0.1.0 // indirect
+ github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
diff --git a/go.sum b/go.sum
index 486b0de..f9353c1 100644
--- a/go.sum
+++ b/go.sum
@@ -598,6 +598,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/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA=
github.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A=
+github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
+github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=
github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=