Files
panel/pkg/acme/solvers.go
T

204 lines
4.9 KiB
Go

package acme
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/libdns/dnspod"
"github.com/libdns/libdns"
"github.com/libdns/tencentcloud"
"github.com/mholt/acmez/v2/acme"
"golang.org/x/net/publicsuffix"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
type httpSolver struct {
conf string
path string
}
func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
var err error
if s.path == "" {
return nil
}
challengeFilePath := filepath.Join(s.path, challenge.HTTP01ResourcePath())
if err = os.MkdirAll(filepath.Dir(challengeFilePath), 0755); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的目录: %w", err)
}
if err = os.WriteFile(challengeFilePath, []byte(challenge.KeyAuthorization), 0644); err != nil {
return fmt.Errorf("无法在网站目录创建HTTP挑战所需的文件: %w", err)
}
conf := fmt.Sprintf(`location = /.well-known/acme-challenge/%s {
default_type text/plain;
return 200 %q;
}
`, challenge.Token, challenge.KeyAuthorization)
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
return fmt.Errorf("无法写入OpenResty配置文件: %w", err)
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return fmt.Errorf("无法重载OpenResty: %w", err)
}
return nil
}
// CleanUp cleans up the HTTP server if it is the last one to finish.
func (s httpSolver) CleanUp(_ context.Context, challenge acme.Challenge) error {
if s.path == "" {
return nil
}
_ = os.Remove(filepath.Join(s.path, challenge.HTTP01ResourcePath()))
_ = os.WriteFile(s.conf, []byte{}, 0644)
_ = systemctl.Reload("nginx")
return nil
}
type dnsSolver struct {
dns DnsType
param DNSParam
records *[]libdns.Record
}
func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
keyAuth := challenge.DNS01KeyAuthorization()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取DNS提供商失败: %w", err)
}
zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName)
if err != nil {
return fmt.Errorf("获取域名%q的顶级域失败: %w", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Name: libdns.RelativeName(dnsName+".", zone+"."),
Value: keyAuth,
}
results, err := provider.AppendRecords(ctx, zone+".", []libdns.Record{rec})
if err != nil {
return fmt.Errorf("域名%q添加临时记录%q失败: %w", zone, dnsName, err)
}
if len(results) != 1 {
return fmt.Errorf("预期添加1条记录,但实际添加了%d条记录", len(results))
}
s.records = &results
return nil
}
func (s dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
provider, err := s.getDNSProvider()
if err != nil {
return fmt.Errorf("获取DNS提供商失败: %w", err)
}
zone, _ := publicsuffix.EffectiveTLDPlusOne(dnsName)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
_, _ = provider.DeleteRecords(ctx, zone+".", *s.records)
return nil
}
func (s dnsSolver) getDNSProvider() (DNSProvider, error) {
var dns DNSProvider
switch s.dns {
case DnsPod:
dns = &dnspod.Provider{
APIToken: s.param.ID + "," + s.param.Token,
}
case Tencent:
dns = &tencentcloud.Provider{
SecretId: s.param.AccessKey,
SecretKey: s.param.SecretKey,
}
case AliYun:
dns = &alidns.Provider{
AccKeyID: s.param.AccessKey,
AccKeySecret: s.param.SecretKey,
}
case CloudFlare:
dns = &cloudflare.Provider{
APIToken: s.param.APIkey,
}
default:
return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns)
}
return dns, nil
}
type DnsType string
const (
DnsPod DnsType = "dnspod"
Tencent DnsType = "tencent"
AliYun DnsType = "aliyun"
CloudFlare DnsType = "cloudflare"
)
type DNSParam struct {
ID string `form:"id" json:"id"`
Token string `form:"token" json:"token"`
AccessKey string `form:"access_key" json:"access_key"`
SecretKey string `form:"secret_key" json:"secret_key"`
APIkey string `form:"api_key" json:"api_key"`
}
type DNSProvider interface {
libdns.RecordAppender
libdns.RecordDeleter
}
type manualDNSSolver struct {
check bool
controlChan chan struct{}
dataChan chan any
records *[]DNSRecord
}
func (s manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
keyAuth := challenge.DNS01KeyAuthorization()
*s.records = append(*s.records, DNSRecord{
Key: dnsName,
Value: keyAuth,
})
s.dataChan <- *s.records
_, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
<-s.controlChan
return nil
}
func (s manualDNSSolver) CleanUp(_ context.Context, _ acme.Challenge) error {
return nil
}
type DNSRecord struct {
Key string `json:"key"`
Value string `json:"value"`
}