From 389b0a005b8a544be1ba3809dfbd3b197cef272c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sat, 19 Oct 2024 03:03:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(HTTPS):=20=E4=BC=98=E5=8C=96=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E7=94=B3=E8=AF=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/data/cert.go | 8 ++- internal/service/cert.go | 4 -- pkg/acme/client.go | 3 +- pkg/acme/client_test.go | 2 +- pkg/acme/solvers.go | 95 ++++++++++++------------------- web/src/i18n/zh_CN.json | 2 +- web/src/views/cert/CertView.vue | 22 ++++++-- web/src/views/cert/DnsView.vue | 99 +++++++++++---------------------- web/src/views/cert/types.ts | 7 +-- 9 files changed, 96 insertions(+), 146 deletions(-) diff --git a/internal/data/cert.go b/internal/data/cert.go index 21171b1c..fb217ed9 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -93,7 +93,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) { } } conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) + client.UseHTTP(conf) } } @@ -172,7 +172,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { } } conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) + client.UseHTTP(conf) } } @@ -251,6 +251,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error { } func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) { + if cert.Account == nil { + return nil, errors.New("该证书没有关联账号,无法签发") + } + var ca string var eab *acme.EAB switch cert.Account.CA { diff --git a/internal/service/cert.go b/internal/service/cert.go index f7b416a7..534300fc 100644 --- a/internal/service/cert.go +++ b/internal/service/cert.go @@ -54,10 +54,6 @@ func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) { Success(w, []types.LV{ - { - Label: "DNSPod", - Value: string(acme.DnsPod), - }, { Label: "腾讯云", Value: string(acme.Tencent), diff --git a/pkg/acme/client.go b/pkg/acme/client.go index 458d7154..93fcbf74 100644 --- a/pkg/acme/client.go +++ b/pkg/acme/client.go @@ -51,11 +51,10 @@ func (c *Client) UseManualDns(total int, check ...bool) { // UseHTTP 使用 HTTP 验证 // conf nginx 配置文件路径 // path 验证文件存放路径 -func (c *Client) UseHTTP(conf, path string) { +func (c *Client) UseHTTP(conf string) { c.zClient.ChallengeSolvers = map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: httpSolver{ conf: conf, - path: path, }, } } diff --git a/pkg/acme/client_test.go b/pkg/acme/client_test.go index 8cdfd8f2..999c43b6 100644 --- a/pkg/acme/client_test.go +++ b/pkg/acme/client_test.go @@ -20,7 +20,7 @@ func (s *ClientTestSuite) TestObtainSSL() { client, err := NewRegisterAccount(ctx, "ci@haozi.net", CALetsEncryptStaging, nil, KeyEC256) s.Nil(err) - client.UseDns(DnsPod, DNSParam{ + client.UseDns(AliYun, DNSParam{ ID: "123456", Token: "654321", }) diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index dcd55a85..cd3a8218 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -4,12 +4,11 @@ import ( "context" "fmt" "os" - "path/filepath" + "strings" "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" @@ -21,35 +20,20 @@ import ( 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 { + conf := fmt.Sprintf(`location = %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("无法写入Nginx配置文件: %w", err) +`, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization) + if err := os.WriteFile(s.conf, []byte(conf), 0644); err != nil { + return fmt.Errorf("无法写入 Nginx 配置文件: %w", err) } - if err = systemctl.Reload("nginx"); err != nil { + if err := systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") - return fmt.Errorf("无法重载Nginx: %w", err) + return fmt.Errorf("无法重载 Nginx: %w", err) } return nil @@ -57,11 +41,6 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error { // 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 @@ -78,11 +57,11 @@ 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("获取 DNS 提供商失败: %w", err) } zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName) if err != nil { - return fmt.Errorf("获取域名%q的顶级域失败: %w", dnsName, err) + return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err) } rec := libdns.Record{ @@ -91,12 +70,12 @@ func (s dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error Value: keyAuth, } - results, err := provider.AppendRecords(ctx, zone+".", []libdns.Record{rec}) + results, err := provider.SetRecords(ctx, zone+".", []libdns.Record{rec}) if err != nil { - return fmt.Errorf("域名%q添加临时记录%q失败: %w", zone, dnsName, err) + return fmt.Errorf("域名 %q 添加临时记录 %q 失败: %w", zone, dnsName, err) } if len(results) != 1 { - return fmt.Errorf("预期添加1条记录,但实际添加了%d条记录", len(results)) + return fmt.Errorf("预期添加 1 条记录,但实际添加了 %d 条记录", len(results)) } s.records = &results @@ -107,13 +86,16 @@ 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("获取 DNS 提供商失败: %w", err) } - zone, _ := publicsuffix.EffectiveTLDPlusOne(dnsName) ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() + zone, err := publicsuffix.EffectiveTLDPlusOne(dnsName) + if err != nil { + return fmt.Errorf("获取域名 %q 的顶级域失败: %w", dnsName, err) + } _, _ = provider.DeleteRecords(ctx, zone+".", *s.records) return nil } @@ -122,23 +104,19 @@ 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, + SecretId: s.param.AK, + SecretKey: s.param.SK, } case AliYun: dns = &alidns.Provider{ - AccKeyID: s.param.AccessKey, - AccKeySecret: s.param.SecretKey, + AccKeyID: s.param.AK, + AccKeySecret: s.param.SK, } case CloudFlare: dns = &cloudflare.Provider{ - APIToken: s.param.APIkey, + APIToken: s.param.AK, } default: return nil, fmt.Errorf("未知的DNS提供商 %q", s.dns) @@ -150,22 +128,18 @@ func (s dnsSolver) getDNSProvider() (DNSProvider, error) { 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"` + AK string `form:"ak" json:"ak"` + SK string `form:"sk" json:"sk"` } type DNSProvider interface { - libdns.RecordAppender + libdns.RecordSetter libdns.RecordDeleter } @@ -177,18 +151,20 @@ type manualDNSSolver struct { } func (s manualDNSSolver) Present(ctx context.Context, challenge acme.Challenge) error { - dnsName := challenge.DNS01TXTRecordName() + full := challenge.DNS01TXTRecordName() keyAuth := challenge.DNS01KeyAuthorization() + domain, err := publicsuffix.EffectiveTLDPlusOne(full) + if err != nil { + return fmt.Errorf("获取 %q 的顶级域失败: %w", full, err) + } *s.records = append(*s.records, DNSRecord{ - Key: dnsName, - Value: keyAuth, + Name: strings.TrimSuffix(full, "."+domain), + Domain: domain, + Value: keyAuth, }) s.dataChan <- *s.records - _, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - <-s.controlChan return nil } @@ -198,6 +174,7 @@ func (s manualDNSSolver) CleanUp(_ context.Context, _ acme.Challenge) error { } type DNSRecord struct { - Key string `json:"key"` - Value string `json:"value"` + Name string `json:"name"` + Domain string `json:"domain"` + Value string `json:"value"` } diff --git a/web/src/i18n/zh_CN.json b/web/src/i18n/zh_CN.json index bfb9933e..65fa3474 100644 --- a/web/src/i18n/zh_CN.json +++ b/web/src/i18n/zh_CN.json @@ -1,7 +1,7 @@ { "name": "耗子面板", "certIndex": { - "title": "TLS 证书" + "title": "HTTPS 证书" }, "containerIndex": { "title": "容器管理" diff --git a/web/src/views/cert/CertView.vue b/web/src/views/cert/CertView.vue index e11bbbc3..8b758ccc 100644 --- a/web/src/views/cert/CertView.vue +++ b/web/src/views/cert/CertView.vue @@ -192,16 +192,26 @@ const certColumns: any = [ window.$message.info('请先前往域名处设置 DNS 解析,再继续签发') const d = window.$dialog.info({ style: 'width: 60vw', - title: 'DNS 记录列表', + title: '待设置DNS 记录列表', content: () => { return h(NTable, [ h('thead', [ - h('tr', [h('th', '域名'), h('th', '类型'), h('th', '记录值')]) + h('tr', [ + h('th', '域名'), + h('th', '类型'), + h('th', '主机记录'), + h('th', '记录值') + ]) ]), h( 'tbody', data.map((item: any) => - h('tr', [h('td', item?.key), h('td', 'TXT'), h('td', item?.value)]) + h('tr', [ + h('td', item?.domain), + h('td', 'TXT'), + h('td', item?.name), + h('td', item?.value) + ]) ) ) ]) @@ -215,7 +225,7 @@ const certColumns: any = [ cert .obtain(row.id) .then(() => { - window.$message.success('签发成功,请前往网站管理启用HTTPS') + window.$message.success('签发成功') onCertPageChange(1) }) .finally(() => { @@ -228,7 +238,7 @@ const certColumns: any = [ cert .obtain(row.id) .then(() => { - window.$message.success('签发成功,请前往网站管理启用HTTPS') + window.$message.success('签发成功') onCertPageChange(1) }) .finally(() => { @@ -417,7 +427,7 @@ const handleUpdateCert = async () => { const handleDeployCert = async () => { await cert.deploy(deployCertModel.value.id, deployCertModel.value.website_id) - window.$message.success('部署成功,请前往网站管理启用HTTPS') + window.$message.success('部署成功') deployCertModal.value = false deployCertModel.value.id = 0 deployCertModel.value.website_id = 0 diff --git a/web/src/views/cert/DnsView.vue b/web/src/views/cert/DnsView.vue index 5d0d7ffd..71838925 100644 --- a/web/src/views/cert/DnsView.vue +++ b/web/src/views/cert/DnsView.vue @@ -6,24 +6,18 @@ import type { DNS } from '@/views/cert/types' const addDNSModel = ref({ data: { - token: '', - id: '', - access_key: '', - api_key: '', - secret_key: '' + ak: '', + sk: '' }, - type: 'dnspod', + type: 'tencent', name: '' }) const updateDNSModel = ref({ data: { - token: '', - id: '', - access_key: '', - api_key: '', - secret_key: '' + ak: '', + sk: '' }, - type: 'dnspod', + type: 'tencent', name: '' }) const addDNSModal = ref(false) @@ -56,8 +50,6 @@ const dnsColumns: any = [ { default: () => { switch (row.type) { - case 'dnspod': - return 'DnsPod' case 'tencent': return '腾讯云' case 'aliyun': @@ -87,11 +79,8 @@ const dnsColumns: any = [ type: 'primary', onClick: () => { updateDNS.value = row.id - updateDNSModel.value.data.token = row.dns_param.token - updateDNSModel.value.data.id = row.dns_param.id - updateDNSModel.value.data.access_key = row.dns_param.access_key - updateDNSModel.value.data.api_key = row.dns_param.api_key - updateDNSModel.value.data.secret_key = row.dns_param.secret_key + updateDNSModel.value.data.ak = row.dns_param.ak + updateDNSModel.value.data.sk = row.dns_param.sk updateDNSModel.value.type = row.type updateDNSModel.value.name = row.name updateDNSModal.value = true @@ -169,11 +158,8 @@ const handleAddDNS = async () => { window.$message.success('添加成功') addDNSModal.value = false onDnsPageChange(1) - addDNSModel.value.data.token = '' - addDNSModel.value.data.id = '' - addDNSModel.value.data.access_key = '' - addDNSModel.value.data.api_key = '' - addDNSModel.value.data.secret_key = '' + addDNSModel.value.data.ak = '' + addDNSModel.value.data.sk = '' addDNSModel.value.name = '' } @@ -182,11 +168,8 @@ const handleUpdateDNS = async () => { window.$message.success('更新成功') updateDNSModal.value = false onDnsPageChange(1) - updateDNSModel.value.data.token = '' - updateDNSModel.value.data.id = '' - updateDNSModel.value.data.access_key = '' - updateDNSModel.value.data.api_key = '' - updateDNSModel.value.data.secret_key = '' + updateDNSModel.value.data.ak = '' + updateDNSModel.value.data.sk = '' updateDNSModel.value.name = '' } @@ -245,57 +228,41 @@ onMounted(async () => { :options="dnsProviders" /> - + - - - - - - - + - + - + - + { :options="dnsProviders" /> - + - + - + - + {