diff --git a/go.mod b/go.mod index 5bd99318..7b82c0b1 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,14 @@ require ( github.com/lib/pq v1.10.9 github.com/libdns/alidns v1.0.3 github.com/libdns/cloudflare v0.1.3 + github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20 + github.com/libdns/godaddy v1.0.3 github.com/libdns/huaweicloud v0.3.1 github.com/libdns/libdns v0.2.3 + github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072 + github.com/libdns/namedotcom v0.3.3 + github.com/libdns/namesilo v0.1.1 + github.com/libdns/porkbun v0.2.0 github.com/libdns/tencentcloud v1.2.0 github.com/mholt/acmez/v3 v3.1.1 github.com/ncruces/go-sqlite3 v0.24.1 @@ -51,6 +57,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/G-Core/gcore-dns-sdk-go v0.2.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect @@ -67,6 +74,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/ncruces/julianday v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1096 // indirect @@ -75,6 +83,7 @@ require ( github.com/tklauser/numcpus v0.9.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 7c015cc8..ec94f226 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/G-Core/gcore-dns-sdk-go v0.2.9 h1:LMMZIRX8y3aJJuAviNSpFmLbovZUw+6Om+8VElp1F90= +github.com/G-Core/gcore-dns-sdk-go v0.2.9/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4= github.com/bddjr/hlfhr v1.3.8 h1:QQ6KYgtnBbvYvCWuu/tOnBZamKAPtJzesj2qbjgyn7o= github.com/bddjr/hlfhr v1.3.8/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= @@ -71,6 +73,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -89,11 +93,24 @@ github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= github.com/libdns/cloudflare v0.1.3 h1:XPFa2f3Mm/3FDNwl9Ki2bfAQJ0Cm5GQB0e8PQVy25Us= github.com/libdns/cloudflare v0.1.3/go.mod h1:XbvSCSMcxspwpSialM3bq0LsS3/Houy9WYxW8Ok8b6M= +github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20 h1:bQwFw+C9sX/zYZlV53ey0KnNkxrfWYIFpvptuAVhJ1Y= +github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20/go.mod h1:JGoT1mbmqQwtYQqN5F/vGc9j4TTTMKw/hDm5vXADHUI= +github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44= +github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM= github.com/libdns/huaweicloud v0.3.1 h1:wro0zpG86JKL3QVEaV/xfHW/59rmqwMYhcbl16w9EYg= github.com/libdns/huaweicloud v0.3.1/go.mod h1:6s5ZIwLUr2qKsQz2SRNo7uh8X5uJohxoBR95rtSpNcI= github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/libdns/libdns v0.2.2-0.20230227175549-2dc480633939/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8= github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072 h1:hCPOoLd9Mr5kAWLofhSCuGYZxAVVy0CV9zgbahzIeSg= +github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072/go.mod h1:jo2LWZSD/g/scevxNSp2kWtdMIXfsgWxrtSc3Z0Yy/I= +github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE= +github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s= +github.com/libdns/namesilo v0.1.1 h1:G6ECxNXpphWDVhSXyAdQBLA4KpHU0Az+TB9YMZMMH/4= +github.com/libdns/namesilo v0.1.1/go.mod h1:JSyG04w+33JDbA2fYWaD9A4zufcxwK7ATNQ1RDuZbps= +github.com/libdns/porkbun v0.2.0 h1:oa2F0doE93RiJkauVVbM+P2AZ7jovDSiH6u3aaQezvQ= +github.com/libdns/porkbun v0.2.0/go.mod h1:mwrhwXpsSA2Xw23t9+qmgFnz+erkn25Sxuh7IBA+x2I= github.com/libdns/tencentcloud v1.2.0 h1:SBbZ9gUZ+ba/p2d8NpiOcj+WZTdYqB5Fz7zypdbK3YU= github.com/libdns/tencentcloud v1.2.0/go.mod h1:o0+WCxQ7LGLtyjnjYU4HbGW9uVjN44SdUDhxdUYLGPw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -110,6 +127,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= diff --git a/internal/biz/cert_dns.go b/internal/biz/cert_dns.go index a18b461d..f32ac2f1 100644 --- a/internal/biz/cert_dns.go +++ b/internal/biz/cert_dns.go @@ -10,7 +10,7 @@ import ( type CertDNS struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"not null;default:''" json:"name"` // 备注名称 - Type string `gorm:"not null;default:'aliyun'" json:"type"` // DNS 提供商 (tencent, aliyun, cloudflare) + Type acme.DnsType `gorm:"not null;default:'aliyun'" json:"type"` // DNS 提供商 Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/internal/data/cert.go b/internal/data/cert.go index e63ccd6c..d03ada3a 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -158,7 +158,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) { } if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + client.UseDns(cert.DNS.Type, cert.DNS.Data) } else { if cert.Website == nil { return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually") @@ -273,7 +273,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { } if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + client.UseDns(cert.DNS.Type, cert.DNS.Data) } else { if cert.Website == nil { return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually") diff --git a/internal/http/request/cert_dns.go b/internal/http/request/cert_dns.go index eec1fd24..026b0807 100644 --- a/internal/http/request/cert_dns.go +++ b/internal/http/request/cert_dns.go @@ -3,14 +3,14 @@ package request import "github.com/tnb-labs/panel/pkg/acme" type CertDNSCreate struct { - Type string `form:"type" json:"type" validate:"required"` + Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom"` Name string `form:"name" json:"name" validate:"required"` Data acme.DNSParam `form:"data" json:"data" validate:"required"` } type CertDNSUpdate struct { ID uint `form:"id" json:"id" validate:"required|exists:cert_dns,id"` - Type string `form:"type" json:"type" validate:"required"` + Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom"` Name string `form:"name" json:"name" validate:"required"` Data acme.DNSParam `form:"data" json:"data" validate:"required"` } diff --git a/internal/service/cert.go b/internal/service/cert.go index 0119d500..072d34de 100644 --- a/internal/service/cert.go +++ b/internal/service/cert.go @@ -69,6 +69,30 @@ func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) { Label: "CloudFlare", Value: string(acme.CloudFlare), }, + { + Label: "Godaddy", + Value: string(acme.Godaddy), + }, + { + Label: "Gcore", + Value: string(acme.Gcore), + }, + { + Label: "Porkbun", + Value: string(acme.Porkbun), + }, + { + Label: "Namecheap", + Value: string(acme.Namecheap), + }, + { + Label: "NameSilo", + Value: string(acme.NameSilo), + }, + { + Label: "Name.com", + Value: string(acme.Namecom), + }, }) } diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index 85df3103..03e3a49e 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -9,8 +9,14 @@ import ( "github.com/libdns/alidns" "github.com/libdns/cloudflare" + "github.com/libdns/gcore" + "github.com/libdns/godaddy" "github.com/libdns/huaweicloud" "github.com/libdns/libdns" + "github.com/libdns/namecheap" + "github.com/libdns/namedotcom" + "github.com/libdns/namesilo" + "github.com/libdns/porkbun" "github.com/libdns/tencentcloud" "github.com/mholt/acmez/v3/acme" "golang.org/x/net/publicsuffix" @@ -30,11 +36,11 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error { } `, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization) if err := os.WriteFile(s.conf, []byte(conf), 0644); err != nil { - return fmt.Errorf("无法写入 Nginx 配置文件: %w", err) + return fmt.Errorf("failed to write nginx config %q: %w", s.conf, err) } if err := systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") - return fmt.Errorf("无法重载 Nginx: %w", err) + return fmt.Errorf("failed to reload nginx: %w", err) } return nil @@ -58,25 +64,26 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error keyAuth := challenge.DNS01KeyAuthorization() provider, err := s.getDNSProvider() if err != nil { - return fmt.Errorf("获取 DNS 提供商失败: %w", err) + return fmt.Errorf("failed to get DNS provider: %w", err) } zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName) if err != nil { - return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err) + return fmt.Errorf("failed to get the effective TLD+1 for %q: %w", dnsName, err) } rec := libdns.Record{ Type: "TXT", Name: libdns.RelativeName(dnsName+".", zone+"."), Value: keyAuth, + TTL: 10 * time.Minute, } results, err := provider.SetRecords(ctx, zone+".", []libdns.Record{rec}) if err != nil { - return fmt.Errorf("域名 %q 添加临时记录 %q 失败: %w", zone, dnsName, err) + return fmt.Errorf("failed to set DNS record %q for %q: %w", dnsName, zone, err) } if len(results) != 1 { - return fmt.Errorf("预期添加 1 条记录,但实际添加了 %d 条记录", len(results)) + return fmt.Errorf("expected to add 1 record, but actually added %d records", len(results)) } s.records = &results @@ -87,7 +94,7 @@ 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) + return fmt.Errorf("failed to get DNS provider: %w", err) } ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) @@ -95,8 +102,9 @@ func (s dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName) if err != nil { - return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err) + return fmt.Errorf("failed to get the effective TLD+1 for %q: %w", dnsName, err) } + _, _ = provider.DeleteRecords(ctx, zone+".", *s.records) return nil } @@ -124,8 +132,36 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) { dns = &cloudflare.Provider{ APIToken: s.param.AK, } + case Godaddy: + dns = &godaddy.Provider{ + APIToken: s.param.AK, + } + case Gcore: + dns = &gcore.Provider{ + APIKey: s.param.AK, + } + case Porkbun: + dns = &porkbun.Provider{ + APIKey: s.param.AK, + APISecretKey: s.param.SK, + } + case Namecheap: + dns = &namecheap.Provider{ + APIKey: s.param.AK, + User: s.param.SK, + } + case NameSilo: + dns = &namesilo.Provider{ + APIToken: s.param.AK, + } + case Namecom: + dns = &namedotcom.Provider{ + Token: s.param.AK, + User: s.param.SK, + Server: "https://api.name.com", + } default: - return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns) + return nil, fmt.Errorf("unsupported DNS provider: %s", s.dns) } return dns, nil @@ -134,10 +170,16 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) { type DnsType string const ( - Tencent DnsType = "tencent" AliYun DnsType = "aliyun" + Tencent DnsType = "tencent" Huawei DnsType = "huawei" CloudFlare DnsType = "cloudflare" + Godaddy DnsType = "godaddy" + Gcore DnsType = "gcore" + Porkbun DnsType = "porkbun" + Namecheap DnsType = "namecheap" + NameSilo DnsType = "namesilo" + Namecom DnsType = "namecom" ) type DNSParam struct { @@ -162,7 +204,7 @@ func (s manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge) keyAuth := challenge.DNS01KeyAuthorization() domain, err := publicsuffix.EffectiveTLDPlusOne(full) if err != nil { - return fmt.Errorf("获取 %q 的顶级域失败: %w", full, err) + return fmt.Errorf("failed to get the effective TLD+1 for %q: %w", full, err) } *s.records = append(*s.records, DNSRecord{ diff --git a/web/src/views/cert/CreateDnsModal.vue b/web/src/views/cert/CreateDnsModal.vue index 63fafae9..741669e4 100644 --- a/web/src/views/cert/CreateDnsModal.vue +++ b/web/src/views/cert/CreateDnsModal.vue @@ -119,6 +119,78 @@ const handleCreateDNS = async () => { placeholder="输入 Cloudflare API Key" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 diff --git a/web/src/views/cert/DnsView.vue b/web/src/views/cert/DnsView.vue index 5286e8cd..a52e0414 100644 --- a/web/src/views/cert/DnsView.vue +++ b/web/src/views/cert/DnsView.vue @@ -46,17 +46,11 @@ const columns: any = [ }, { default: () => { - switch (row.type) { - case 'aliyun': - return '阿里云' - case 'tencent': - return '腾讯云' - case 'huawei': - return '华为云' - case 'cloudflare': - return 'Cloudflare' - default: - return '未知' + const provider = dnsProviders.value.find((provider: any) => provider.value === row.type) + if (provider) { + return provider.label + } else { + return '未知' } } } @@ -206,7 +200,6 @@ onUnmounted(() => { :options="dnsProviders" /> - { placeholder="输入腾讯云 SecretKey" /> - { placeholder="输入 Cloudflare API Key" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + 提交 diff --git a/web/src/views/cert/ObtainModal.vue b/web/src/views/cert/ObtainModal.vue index d270a1d3..e6be1acf 100644 --- a/web/src/views/cert/ObtainModal.vue +++ b/web/src/views/cert/ObtainModal.vue @@ -34,49 +34,64 @@ const handleSubmit = () => { messageReactive?.destroy() }) } else if (model.value.type == 'manual') { - const { data } = useRequest(cert.manualDNS(id.value)) - messageReactive.destroy() - window.$message.info('请先前往域名处设置 DNS 解析,再继续签发') - const d = window.$dialog.info({ - style: 'width: 60vw', - title: '待设置DNS 记录列表', - content: () => { - return h(NTable, [ - h('thead', [ - h('tr', [h('th', '域名'), h('th', '类型'), h('th', '主机记录'), h('th', '记录值')]) - ]), - h( - 'tbody', - data.map((item: any) => - h('tr', [ - h('td', item?.domain), - h('td', 'TXT'), - h('td', item?.name), - h('td', item?.value) - ]) + useRequest(cert.manualDNS(id.value)) + .onSuccess(({ data }: { data: any }) => { + window.$message.info('请先前往域名处设置 DNS 解析,再继续签发') + const d = window.$dialog.info({ + style: 'width: 60vw', + title: '待设置DNS 记录列表', + content: () => { + return h( + NTable, + {}, + { + default: () => [ + h('thead', [ + h('tr', [ + h('th', '域名'), + h('th', '类型'), + h('th', '主机记录'), + h('th', '记录值') + ]) + ]), + h( + 'tbody', + data.map((item) => + h('tr', [ + h('td', item?.domain), + h('td', 'TXT'), + h('td', item?.name), + h('td', item?.value) + ]) + ) + ) + ] + } ) - ) - ]) - }, - positiveText: '签发', - onPositiveClick: async () => { - d.loading = true - messageReactive = window.$message.loading('请稍后...', { - duration: 0 + }, + positiveText: '签发', + onPositiveClick: async () => { + d.loading = true + messageReactive = window.$message.loading('请稍后...', { + duration: 0 + }) + useRequest(cert.obtainManual(id.value)) + .onSuccess(() => { + window.$bus.emit('cert:refresh-cert') + window.$bus.emit('cert:refresh-async') + show.value = false + window.$message.success('签发成功') + }) + .onComplete(() => { + d.loading = false + messageReactive?.destroy() + }) + } }) - useRequest(cert.obtainManual(id.value)) - .onSuccess(() => { - window.$bus.emit('cert:refresh-cert') - window.$bus.emit('cert:refresh-async') - show.value = false - window.$message.success('签发成功') - }) - .onComplete(() => { - d.loading = false - messageReactive?.destroy() - }) - } - }) + }) + .onComplete(() => { + messageReactive?.destroy() + }) } else { useRequest(cert.obtainSelfSigned(id.value)) .onSuccess(() => {