【新增】宝塔dns

1.1.0 v1.1.0
v-me-50 2025-09-17 15:36:41 +08:00
parent 5b7245a78d
commit e4b456b1ee
2 changed files with 278 additions and 0 deletions

View File

@ -0,0 +1,81 @@
package bt
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// 生成 API 签名
func (c *Config) generateSignature(method, path string, body string) (string, string) {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
signingString := fmt.Sprintf("%s\n%s\n%s\n%s\n%s",
c.AccountID,
timestamp,
strings.ToUpper(method),
path,
body,
)
h := hmac.New(sha256.New, []byte(c.SecretKey))
h.Write([]byte(signingString))
signature := hex.EncodeToString(h.Sum(nil))
return timestamp, signature
}
// 发起 API 请求
func (c *Config) MakeRequest(method, path string, data interface{}) (map[string]interface{}, error) {
url := strings.TrimRight(c.BaseURL, "/") + path
var bodyStr string
var bodyBytes []byte
if data != nil {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
bodyStr = string(b)
bodyBytes = b
}
timestamp, signature := c.generateSignature(method, path, bodyStr)
req, err := http.NewRequest(method, url, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Account-ID", c.AccountID)
req.Header.Set("X-Access-Key", c.AccessKey)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Signature", signature)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(respBytes, &result); err != nil {
return nil, err
}
return result, nil
}

View File

@ -0,0 +1,197 @@
package bt
import (
"fmt"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"time"
)
const (
envNamespace = "BTDOMAIN_"
EnvAccountID = envNamespace + "ACCOUNT_ID"
EnvAccessKey = envNamespace + "ACCESS_KEY"
EnvSecretKey = envNamespace + "SECRET_KEY"
EnvBaseURL = envNamespace + "BASE_URL"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AccountID string
AccessKey string
SecretKey string
BaseURL string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
func NewConfig(accountID, accessKey, secretKey, baseURL string) *Config {
return &Config{
AccountID: accountID,
AccessKey: accessKey,
SecretKey: secretKey,
BaseURL: baseURL,
TTL: 600,
PropagationTimeout: dns01.DefaultPropagationTimeout,
PollingInterval: dns01.DefaultPollingInterval,
HTTPTimeout: 30 * time.Second,
}
}
func NewDefaultConfig() *Config {
return &Config{
BaseURL: env.GetOrDefaultString(EnvBaseURL, "https://dmp.bt.cn"),
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
type DNSProvider struct {
config *Config
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAccountID, EnvAccessKey, EnvSecretKey)
if err != nil {
return nil, fmt.Errorf("westcn: %w", err)
}
config := NewDefaultConfig()
config.AccountID = values[EnvAccountID]
config.AccessKey = values[EnvAccessKey]
config.SecretKey = values[EnvSecretKey]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, nil
}
return &DNSProvider{config: config}, nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return d.config.addDNSRecord(domain, keyAuth)
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return d.config.removeDNSRecord(domain, keyAuth)
}
func (c *Config) GetDomainId(domain string) (int, int) {
domain = dns01.UnFqdn(domain)
resp, err := c.MakeRequest("POST", "/api/v1/dns/manage/list_domains", map[string]interface{}{
"p": "1",
"rows": "100",
"keyword": domain,
})
if err != nil {
return 0, 0
}
if !resp["status"].(bool) {
return 0, 0
}
data := resp["data"].(map[string]interface{})
list := data["data"].([]interface{})
for _, item := range list {
d := item.(map[string]interface{})
if d["full_domain"].(string) == domain {
return int(d["local_id"].(float64)), int(d["domain_type"].(float64))
}
}
return 0, 0
}
func (c *Config) addDNSRecord(domain, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
EffectiveFQDN := dns01.UnFqdn(info.EffectiveFQDN)
rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("无法获取域名的根域名: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(EffectiveFQDN, rootDomain)
if err != nil {
return fmt.Errorf("无法获取域名的子域名: %w", err)
}
domainId, domainType := c.GetDomainId(rootDomain)
if domainId == 0 {
return nil
}
_, err = c.MakeRequest("POST", "/api/v1/dns/record/create", map[string]interface{}{
"domain_id": domainId,
"domain_type": domainType,
"record": subDomain,
"value": info.Value,
"type": "TXT",
})
return err
}
func (c *Config) removeDNSRecord(domain, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
EffectiveFQDN := dns01.UnFqdn(info.EffectiveFQDN)
rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("无法获取域名的根域名: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(EffectiveFQDN, rootDomain)
if err != nil {
return fmt.Errorf("无法获取域名的子域名: %w", err)
}
domainId, domainType := c.GetDomainId(rootDomain)
if domainId == 0 {
return nil
}
resp, err := c.MakeRequest("POST", "/api/v1/dns/record/list", map[string]interface{}{
"domain_id": domainId,
"domain_type": domainType,
"p": "1",
"rows": "100",
"searchKey": subDomain,
})
if err != nil {
return err
}
if !resp["status"].(bool) {
return nil
}
data := resp["data"].(map[string]interface{})
list := data["data"].([]interface{})
for _, item := range list {
d := item.(map[string]interface{})
if d["record"].(string) == subDomain && d["type"].(string) == "TXT" && d["value"].(string) == info.Value {
_, err = c.MakeRequest("POST", "/api/v1/dns/record/delete", map[string]interface{}{
"domain_id": domainId,
"domain_type": domainType,
"record_id": int(d["record_id"].(float64)),
})
return err
}
}
return nil
}