diff --git a/backend/internal/cli/dns.go b/backend/internal/cli/dns.go index a002376..03be366 100644 --- a/backend/internal/cli/dns.go +++ b/backend/internal/cli/dns.go @@ -44,6 +44,10 @@ func newDNSPermissionCommand(verify dnsPermissionVerifier) *cobra.Command { if !confirmWrite { return errors.New("此检查会写入并删除专用临时 TXT 记录;确认已获目标域名测试授权后,请添加 --confirm-dns-write") } + // 保护域必须在读取配置、凭据或构造云端客户端前拒绝,避免诊断入口产生任何外部副作用。 + if err := dnsprovider.ValidateAliyunTargetDomains(dnsprovider.Aliyun, []string{requestedDomain}); err != nil { + return err + } cfg, err := loadConfig() if err != nil { return err @@ -66,9 +70,6 @@ func newDNSPermissionCommand(verify dnsPermissionVerifier) *cobra.Command { if err != nil { return fmt.Errorf("读取阿里云 DNS 凭据失败:%w", err) } - if err := dnsprovider.ValidateAliyunTargetDomains(dnsprovider.Aliyun, []string{requestedDomain}); err != nil { - return err - } ctx, cancel := context.WithTimeout(cmd.Context(), dnsprovider.PermissionVerificationTimeoutFor(dnsprovider.Aliyun)) defer cancel() report, verifyErr := verify(ctx, requestedDomain, credentials) diff --git a/backend/internal/cli/dns_test.go b/backend/internal/cli/dns_test.go index 326fb93..27ca808 100644 --- a/backend/internal/cli/dns_test.go +++ b/backend/internal/cli/dns_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + "github.com/spf13/cobra" + "git.pcux.cn/xuans/Scert/backend/internal/config" "git.pcux.cn/xuans/Scert/backend/internal/dnsprovider" "git.pcux.cn/xuans/Scert/backend/internal/secretstore" @@ -31,38 +33,38 @@ func TestDNSCheckRequiresExplicitWriteConfirmation(t *testing.T) { } } -func TestDNSCheckRejectsOutsideScopeBeforeVerifier(t *testing.T) { - prepareDNSCheckConfig(t) +func TestDNSCheckRejectsProtectedDomainBeforeConfigurationOrVerifier(t *testing.T) { + oldPath := configPath + configPath = filepath.Join(t.TempDir(), "missing-scert.json") + t.Cleanup(func() { configPath = oldPath }) + command := newDNSCheckCommand(func(context.Context, string, map[string]string) (dnsprovider.AuthorizationReport, error) { - t.Fatal("CLI 越界不得进入云端验证") + t.Fatal("保护域不得进入云端验证") return dnsprovider.AuthorizationReport{}, nil }) command.SilenceUsage, command.SilenceErrors = true, true - command.SetArgs([]string{"--domain", "outside.net", "--confirm-dns-write"}) - if err := command.Execute(); err == nil { - t.Fatal("确认写入不等于允许扩大范围") + command.SetArgs([]string{"--domain", "pcux.cn", "--confirm-dns-write"}) + if err := command.Execute(); err == nil || !strings.Contains(err.Error(), "pcux.cn") { + t.Fatalf("保护域必须在读取配置前被拒绝:%v", err) } } -func TestDNSInspectDoesNotRequireWriteOrPromoteReadableToVerified(t *testing.T) { - prepareDNSCheckConfig(t) - command := newDNSInspectCommand(func(context.Context, string, map[string]string) (dnsprovider.AuthorizationReport, error) { - return dnsprovider.AuthorizationReport{ - Provider: "aliyun", Domains: []dnsprovider.AuthorizationDomain{{Domain: "example.com", Listed: true}}, - }, nil - }) - var output bytes.Buffer - command.SetOut(&output) - command.SetArgs([]string{"--domain", "example.com"}) - if err := command.Execute(); err != nil { - t.Fatal(err) +func TestDNSCommandOnlyExposesConfirmedWriteCheck(t *testing.T) { + command := newDNSCommand() + var check *cobra.Command + for _, child := range command.Commands() { + if child.Name() == "inspect" { + t.Fatal("不得暴露会被误解为可签发授权的只读 inspect 命令") + } + if child.Name() == "check" { + check = child + } } - var report dnsprovider.AuthorizationReport - if err := json.Unmarshal(output.Bytes(), &report); err != nil || report.Verified || report.Domains[0].Writable { - t.Fatalf("只读成功不能升级为写权限通过:%s", output.String()) + if check == nil { + t.Fatal("必须提供 dns check 命令") } - if command.Flag("confirm-dns-write") != nil { - t.Fatal("只读命令不得提供写入开关") + if check.Flag("domain") == nil || check.Flag("confirm-dns-write") == nil { + t.Fatal("dns check 必须同时要求明确域名和写入确认") } } diff --git a/backend/internal/dnsverification/report.go b/backend/internal/dnsverification/report.go new file mode 100644 index 0000000..c8ab610 --- /dev/null +++ b/backend/internal/dnsverification/report.go @@ -0,0 +1,12 @@ +package dnsverification + +type Stage struct { + Name string `json:"name"` + Status string `json:"status"` + Message string `json:"message"` +} + +type Report struct { + RecordName string `json:"record_name"` + Stages []Stage `json:"stages"` +}