Scert/backend/internal/httpapi/setup.go

622 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package httpapi
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
"git.pcux.cn/xuans/Scert/backend/internal/dnsprovider"
"git.pcux.cn/xuans/Scert/backend/internal/firewall"
"git.pcux.cn/xuans/Scert/backend/internal/setup"
)
const setupRequestLimit = 64 * 1024
type setupDNSVerifier interface {
Verify(ctx context.Context, input setup.DNSVerificationInput) (setup.DNSVerificationResult, error)
Check(token, providerID, credentialRef, testDomain string) error
Consume(token, providerID, credentialRef, testDomain string) error
}
type setupRequest struct {
AdminUsername string `json:"admin_username"`
AdminEmail string `json:"admin_email"`
AdminPassword string `json:"admin_password"`
ServerPort int `json:"server_port"`
EmailHTTPEnabled *bool `json:"email_http_enabled"`
EmailSMTPEnabled *bool `json:"email_smtp_enabled"`
EmailSMTPHost string `json:"email_smtp_host"`
EmailSMTPPort int `json:"email_smtp_port"`
EmailSMTPUsername string `json:"email_smtp_username"`
EmailSMTPPassword string `json:"email_smtp_password"`
EmailSMTPMode string `json:"email_smtp_mode"`
EmailSMTPAllowPlaintextAuth bool `json:"email_smtp_allow_plaintext_auth"`
EmailProvider string `json:"email_provider"`
EmailEndpoint string `json:"email_endpoint"`
EmailHTTPPath string `json:"email_http_path"`
EmailHTTPLoginPath string `json:"email_http_login_path"`
EmailHTTPAuthMode string `json:"email_http_auth_mode"`
EmailHTTPCredentialRef string `json:"email_http_credential_ref"`
EmailHTTPCredentials map[string]string `json:"email_http_credentials"`
EmailHTTPHeaders map[string]string `json:"email_http_headers"`
EmailEventMode string `json:"email_event_mode"`
EmailIncludeDeploymentDetails bool `json:"email_include_deployment_details"`
EmailIncludeCertificateSerial bool `json:"email_include_certificate_serial"`
EmailIncludeManagementURL bool `json:"email_include_management_url"`
EmailSenderName string `json:"email_sender_name"`
EmailSenderEmail string `json:"email_sender_email"`
Recipients []string `json:"recipients"`
EmailTimeoutSecs int `json:"email_timeout_secs"`
EmailRetryCount int `json:"email_retry_count"`
EmailRetryIntervalsSecs []int `json:"email_retry_intervals_secs"`
DNSProvider string `json:"dns_provider"`
DNSCredentialRef string `json:"dns_credential_ref"`
DNSVerificationToken string `json:"dns_verification_token"`
DNSTestDomain string `json:"dns_test_domain"`
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
CertificateRoot string `json:"certificate_root"`
WebHostname string `json:"web_hostname"`
WebPublicIP string `json:"web_public_ip"`
WebEnabled *bool `json:"web_enabled"`
HTTPSMode string `json:"https_mode"`
}
type dnsTestRequest struct {
ProviderID string `json:"provider"`
TestDomain string `json:"test_domain"`
Credentials map[string]string `json:"credentials"`
}
func (s *Server) registerSetupRoutes() {
s.router.GET("/api/v1/setup/status", s.setupStatus)
s.router.GET("/api/v1/setup/dns/providers", s.setupDNSProviders)
s.router.POST("/api/v1/setup/dns/test", s.testSetupDNS)
s.router.POST("/api/v1/setup", s.applySetup)
}
func (s *Server) setupStatus(c *gin.Context) {
s.configMu.RLock()
cfg := s.config
s.configMu.RUnlock()
if !cfg.SetupReady && !isLoopbackRequest(c.Request) {
s.writeError(c, http.StatusForbidden, "首次初始化接口仅允许从本机访问", nil)
return
}
phase := "ready"
blockers := []string{}
if !cfg.SetupReady {
phase = "configuration_required"
blockers = append(blockers, "需要完成首次初始化")
if cfg.DNS.VerificationState != "verified" {
blockers = append(blockers, "DNS API 尚未验证")
}
}
c.JSON(http.StatusOK, gin.H{
"initialized": cfg.SetupReady,
"phase": phase,
"blockers": blockers,
"server_port": cfg.Server.Port,
"reset_url": cfg.ResetURL(),
"https_mode": cfg.Server.HTTPSMode,
"web_enabled": cfg.Web.Enabled,
"web_hostname": cfg.Web.Hostname,
"web_public_ip": cfg.Web.PublicIP,
"certificate_root": cfg.Paths.Certificate,
"dns_provider": cfg.DNS.Provider,
"dns_verification_state": cfg.DNS.VerificationState,
"dns_test_available": s.setupDNS != nil,
"propagation_timeout_secs": cfg.ACME.PropagationTimeoutSecs,
"propagation_polling_interval_secs": cfg.ACME.PropagationPollingIntervalSecs,
})
}
func (s *Server) setupDNSProviders(c *gin.Context) {
if !s.allowSetupRequest(c) {
return
}
s.listDNSProviders(c)
}
func (s *Server) listAdminDNSProviders(c *gin.Context) {
s.listDNSProviders(c)
}
func (s *Server) listDNSProviders(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": dnsprovider.Descriptors()})
}
func (s *Server) testSetupDNS(c *gin.Context) {
if !s.allowSetupRequest(c) {
return
}
s.testDNS(c)
}
func (s *Server) testAdminDNS(c *gin.Context) {
s.testDNS(c)
}
func (s *Server) testDNS(c *gin.Context) {
if s.setupDNS == nil {
s.writeError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil)
return
}
var request dnsTestRequest
if err := bindJSON(c, &request, setupRequestLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "DNS 测试请求参数无效", err)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), s.dnsVerificationTimeout())
defer cancel()
result, err := s.setupDNS.Verify(ctx, setup.DNSVerificationInput{
ProviderID: request.ProviderID, TestDomain: request.TestDomain, Credentials: request.Credentials,
})
if err != nil {
if errors.Is(err, dnsprovider.ErrProviderUnavailable) {
s.writeError(c, http.StatusNotImplemented, "该 DNS 服务商在线验证适配器尚未开放", nil)
return
}
report := result.TestReport
var verificationErr *setup.DNSVerificationError
if errors.As(err, &verificationErr) {
report = verificationErr.Report
}
if s.logger != nil {
s.logger.Error("DNS API 在线验证失败", "provider", request.ProviderID, "test_domain", request.TestDomain)
}
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "DNS API 在线验证失败,请检查凭据、测试域名和 DNS 权限", "test_report": report})
return
}
c.JSON(http.StatusOK, gin.H{
"verified": true,
"credential_ref": result.CredentialRef,
"verification_token": result.VerificationToken,
"expires_at": result.ExpiresAt,
"test_report": result.TestReport,
"message": "DNS API 在线验证通过,凭据已加密保存。",
})
}
func (s *Server) dnsVerificationTimeout() time.Duration {
s.configMu.RLock()
cfg := s.config
s.configMu.RUnlock()
timeout := time.Duration(cfg.ACME.PropagationTimeoutSecs)*time.Second + dnsCleanupRequestBuffer
if timeout <= dnsCleanupRequestBuffer {
return defaultDNSRequestTimeout
}
return timeout
}
const defaultDNSRequestTimeout = 45 * time.Second
const dnsCleanupRequestBuffer = 30 * time.Second
func (s *Server) applySetup(c *gin.Context) {
s.configMu.Lock()
defer s.configMu.Unlock()
if !isLoopbackRequest(c.Request) {
s.writeError(c, http.StatusForbidden, "首次初始化接口仅允许从本机访问", nil)
return
}
if s.config.SetupReady {
s.writeError(c, http.StatusConflict, "初始化已经完成,不能重复提交", nil)
return
}
var request setupRequest
if err := bindJSON(c, &request, setupRequestLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "初始化请求参数无效", err)
return
}
if s.setupDNS == nil {
s.writeError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil)
return
}
testDomain := strings.TrimSpace(request.DNSTestDomain)
if testDomain == "" {
testDomain = strings.TrimSpace(request.WebHostname)
}
if err := s.setupDNS.Check(request.DNSVerificationToken, request.DNSProvider, request.DNSCredentialRef, testDomain); err != nil {
s.writeError(c, http.StatusBadRequest, "DNS 验证状态无效,请重新完成在线测试", err)
return
}
dnsCredentialRetained := false
defer func() {
if dnsCredentialRetained || s.secrets == nil {
return
}
cleanupContext, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.secrets.Delete(cleanupContext, strings.TrimSpace(request.DNSCredentialRef)); err != nil && s.logger != nil {
s.logger.Warn("\u521d\u59cb\u5316\u5931\u8d25\u540e\u6e05\u7406 DNS \u51ed\u6362\u5931\u8d25", "error", err)
}
}()
webEnabled := true
if request.WebEnabled != nil {
webEnabled = *request.WebEnabled
}
emailHTTPEnabled := false
if request.EmailHTTPEnabled != nil {
emailHTTPEnabled = *request.EmailHTTPEnabled
}
emailSMTPEnabled := false
if request.EmailSMTPEnabled != nil {
emailSMTPEnabled = *request.EmailSMTPEnabled
}
createdCredentialRefs := make([]string, 0, 2)
emailCredentialRef := strings.TrimSpace(request.EmailHTTPCredentialRef)
smtpCredentialRef := ""
if emailSMTPEnabled {
smtpCredentialRef = strings.TrimSpace(s.config.Notification.SMTPCredentialRef)
if strings.TrimSpace(request.EmailSMTPPassword) != "" {
if strings.TrimSpace(request.EmailSMTPUsername) == "" {
s.writeError(c, http.StatusBadRequest, "填写 SMTP 密码前必须配置 SMTP 用户名", nil)
return
}
if s.secrets == nil {
s.writeError(c, http.StatusServiceUnavailable, "SMTP 通知敏感凭据存储尚未初始化", nil)
return
}
var smtpErr error
smtpCredentialRef, smtpErr = newNotificationCredentialReference()
if smtpErr != nil {
s.writeError(c, http.StatusInternalServerError, "生成 SMTP 通知凭据引用失败", smtpErr)
return
}
createdCredentialRefs = append(createdCredentialRefs, smtpCredentialRef)
}
if smtpCredentialRef == "" {
s.writeError(c, http.StatusBadRequest, "启用 SMTP 通知后必须填写 SMTP 密码或使用已有凭据", nil)
return
}
}
var notificationCredentials map[string]string
var credentialErr error
if emailHTTPEnabled && strings.ToLower(strings.TrimSpace(request.EmailHTTPAuthMode)) != "none" {
if len(request.EmailHTTPCredentials) > 0 {
notificationCredentials, credentialErr = normalizeNotificationCredentials(request.EmailHTTPAuthMode, request.EmailHTTPCredentials)
if credentialErr != nil {
s.writeError(c, http.StatusBadRequest, credentialErr.Error(), nil)
return
}
if s.secrets == nil {
s.writeError(c, http.StatusServiceUnavailable, "HTTP 通知敏感凭据存储尚未初始化", nil)
return
}
emailCredentialRef, credentialErr = newNotificationCredentialReference()
if credentialErr != nil {
s.writeError(c, http.StatusInternalServerError, "生成 HTTP 通知凭据引用失败", credentialErr)
return
}
createdCredentialRefs = append(createdCredentialRefs, emailCredentialRef)
} else if emailCredentialRef == "" {
s.writeError(c, http.StatusBadRequest, "启用 HTTP 通知认证后必须填写明文凭据或已有凭据引用", nil)
return
}
}
updated := s.config
err := setup.Apply(&updated, setup.Input{
AdminUsername: request.AdminUsername,
AdminEmail: request.AdminEmail,
AdminPassword: request.AdminPassword,
ServerPort: request.ServerPort,
EmailHTTPEnabled: emailHTTPEnabled,
EmailSMTPEnabled: emailSMTPEnabled,
EmailSMTPHost: request.EmailSMTPHost,
EmailSMTPPort: request.EmailSMTPPort,
EmailSMTPUsername: request.EmailSMTPUsername,
EmailSMTPCredentialRef: smtpCredentialRef,
EmailSMTPMode: request.EmailSMTPMode,
EmailSMTPAllowPlaintextAuth: request.EmailSMTPAllowPlaintextAuth,
EmailProvider: request.EmailProvider,
EmailEndpoint: request.EmailEndpoint,
EmailHTTPPath: request.EmailHTTPPath,
EmailHTTPLoginPath: request.EmailHTTPLoginPath,
EmailHTTPAuthMode: request.EmailHTTPAuthMode,
EmailHTTPCredentialRef: emailCredentialRef,
EmailHTTPHeaders: request.EmailHTTPHeaders,
EmailEventMode: request.EmailEventMode,
EmailIncludeDeploymentDetails: request.EmailIncludeDeploymentDetails,
EmailIncludeCertificateSerial: request.EmailIncludeCertificateSerial,
EmailIncludeManagementURL: request.EmailIncludeManagementURL,
EmailSenderName: request.EmailSenderName,
EmailSenderEmail: request.EmailSenderEmail,
EmailRecipients: request.Recipients,
EmailTimeoutSecs: request.EmailTimeoutSecs,
EmailRetryCount: request.EmailRetryCount,
EmailRetryIntervalsSecs: request.EmailRetryIntervalsSecs,
DNSProvider: request.DNSProvider,
DNSCredentialRef: request.DNSCredentialRef,
DNSVerified: true,
PropagationTimeoutSecs: request.PropagationTimeoutSecs,
PropagationPollingIntervalSecs: request.PropagationPollingIntervalSecs,
CertificateRoot: request.CertificateRoot,
WebHostname: request.WebHostname,
WebPublicIP: request.WebPublicIP,
WebEnabled: webEnabled,
HTTPSMode: request.HTTPSMode,
})
if err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.writeError(c, http.StatusBadRequest, err.Error(), err)
return
}
if strings.TrimSpace(s.configPath) == "" {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.writeError(c, http.StatusInternalServerError, "服务未配置配置文件路径,无法保存初始化结果", nil)
return
}
if strings.TrimSpace(updated.Web.PublicIP) != "" {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.writeError(c, http.StatusBadRequest, "公网 IP 暂不支持通过 DNS-01 自动申请管理后台证书,请先配置域名或上传自有证书", nil)
return
}
if s.issueSetupCertificate == nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.writeError(c, http.StatusServiceUnavailable, "初始化证书申请服务尚未配置", nil)
return
}
updated.SetupReady = false
certificateContext, certificateCancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
savedCertificate, certificateErr := s.issueSetupCertificate(certificateContext, certificate.SetupCertificateRequest{
Domains: []string{updated.Web.Hostname},
CertificateType: "single",
ProviderID: updated.DNS.Provider,
CredentialRef: updated.DNS.CredentialRef,
AccountEmail: updated.Admin.Email,
Environment: updated.ACME.Environment,
CertificateRoot: updated.Paths.Certificate,
BackupRoot: updated.Paths.Backup,
})
certificateCancel()
if certificateErr != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.writeError(c, http.StatusBadGateway, "申请管理后台证书失败,初始化尚未完成", certificateErr)
return
}
if err := validateSetupCertificateFiles(savedCertificate); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if rollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate); rollbackErr != nil && s.logger != nil {
s.logger.Error("管理后台证书文件校验失败,且证书回滚失败", "error", rollbackErr)
}
s.writeError(c, http.StatusInternalServerError, "管理后台证书文件校验失败:初始化尚未完成", err)
return
}
updated.Server.TLSCert = savedCertificate.FullChainPath
updated.Server.TLSKey = savedCertificate.PrivateKeyPath
if len(notificationCredentials) > 0 {
if err := s.secrets.Put(c.Request.Context(), emailCredentialRef, notificationCredentials); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if rollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate); rollbackErr != nil && s.logger != nil {
s.logger.Error("保存 HTTP 通知凭据失败,且证书回滚失败", "error", rollbackErr)
}
s.writeError(c, http.StatusInternalServerError, "保存 HTTP 通知凭据失败,初始化尚未完成", err)
return
}
}
if emailSMTPEnabled && strings.TrimSpace(request.EmailSMTPPassword) != "" {
if err := s.secrets.Put(c.Request.Context(), smtpCredentialRef, map[string]string{
"username": strings.TrimSpace(request.EmailSMTPUsername),
"password": request.EmailSMTPPassword,
}); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if rollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate); rollbackErr != nil && s.logger != nil {
s.logger.Error("保存 SMTP 通知凭据失败,且证书回滚失败", "error", rollbackErr)
}
s.writeError(c, http.StatusInternalServerError, "保存 SMTP 通知凭据失败,初始化尚未完成", err)
return
}
}
updated.SetupReady = true
if err := updated.Validate(); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if rollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate); rollbackErr != nil && s.logger != nil {
s.logger.Error("初始化证书配置无效,且证书回滚失败", "error", rollbackErr)
}
s.writeError(c, http.StatusInternalServerError, "初始化证书配置无效:初始化尚未完成", err)
return
}
firewallResult := firewall.Result{
Backend: firewall.BackendNone, Port: updated.Server.Port,
Message: "未配置防火墙管理器,已跳过本机规则创建",
}
if s.firewall != nil {
firewallContext, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
firewallResult, err = s.firewall.OpenTCP(firewallContext, updated.Server.Port)
cancel()
if err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if rollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate); rollbackErr != nil && s.logger != nil {
s.logger.Error("自动配置本机防火墙失败,且证书回滚失败", "error", rollbackErr)
}
s.writeError(c, http.StatusInternalServerError, "自动配置本机防火墙失败,请使用管理员权限重新运行初始化程序", err)
return
}
}
if err := updated.Save(s.configPath); err != nil {
firewallRollbackErr := error(nil)
if firewallResult.Created && s.firewall != nil {
rollbackContext, cancel := context.WithTimeout(context.Background(), 30*time.Second)
firewallRollbackErr = s.firewall.Rollback(rollbackContext, firewallResult)
cancel()
if firewallRollbackErr != nil && s.logger != nil {
s.logger.Error("保存初始化配置失败,且回滚本机防火墙规则失败", "error", firewallRollbackErr, "rule", firewallResult.RuleName)
}
}
certificateRollbackErr := s.rollbackIssuedSetupCertificate(savedCertificate)
if certificateRollbackErr != nil && s.logger != nil {
s.logger.Error("保存初始化配置失败,且初始化证书回滚失败", "error", certificateRollbackErr)
}
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
if firewallRollbackErr != nil {
s.writeError(c, http.StatusInternalServerError, "保存初始化配置失败,且自动创建的防火墙规则回滚失败,请手动检查本机防火墙", err)
return
}
if certificateRollbackErr != nil {
s.writeError(c, http.StatusInternalServerError, "保存初始化配置失败,且管理后台证书回滚失败,请手动检查证书目录", err)
return
}
s.writeError(c, http.StatusInternalServerError, "保存初始化配置失败", err)
return
}
dnsCredentialRetained = true
if err := s.setupDNS.Consume(request.DNSVerificationToken, request.DNSProvider, request.DNSCredentialRef, testDomain); err != nil && s.logger != nil {
s.logger.Warn("初始化配置已保存,但清理 DNS 验证令牌失败", "error", err)
}
s.config = updated
c.JSON(http.StatusOK, gin.H{
"initialized": true,
"message": fmt.Sprintf(
"初始化配置已保存,DNS API 已完成在线验证且凭据已加密保存。%s。请确认云安全组已放行主服务端口 %d;Scert-admin 默认关闭,启动后通过 %s 使用临时重置入口。管理后台证书申请成功。",
firewallResult.Message, updated.Server.Port, updated.ResetURL(),
),
"status": gin.H{
"server_port": updated.Server.Port,
"reset_url": updated.ResetURL(),
"web_enabled": updated.Web.Enabled,
"https_mode": updated.Server.HTTPSMode,
"firewall": firewallResult,
},
})
}
func (s *Server) rollbackSetupCredentialsWithLog(references []string) {
if err := s.rollbackSetupCredentials(references); err != nil && s.logger != nil {
s.logger.Error("\u521d\u59cb\u5316\u654f\u611f\u51ed\u636e\u56de\u6eda\u5931\u8d25", "error", err)
}
}
func (s *Server) rollbackSetupCredentials(references []string) error {
if s == nil || s.secrets == nil || len(references) == 0 {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
seen := make(map[string]struct{}, len(references))
var rollbackErrors []error
for _, reference := range references {
reference = strings.TrimSpace(reference)
if reference == "" {
continue
}
if _, ok := seen[reference]; ok {
continue
}
seen[reference] = struct{}{}
if err := s.secrets.Delete(ctx, reference); err != nil {
rollbackErrors = append(rollbackErrors, fmt.Errorf("\u56de\u6eda\u654f\u611f\u51ed\u636e %s \u5931\u8d25\uFF1A%w", reference, err))
}
}
return errors.Join(rollbackErrors...)
}
func (s *Server) rollbackIssuedSetupCertificate(saved certificate.SavedResource) error {
if s.rollbackSetupCertificate == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return s.rollbackSetupCertificate(ctx, saved)
}
func validateSetupCertificateFiles(saved certificate.SavedResource) error {
for label, path := range map[string]string{
"完整证书链": saved.FullChainPath,
"私钥": saved.PrivateKeyPath,
} {
if strings.TrimSpace(path) == "" {
return fmt.Errorf("%s路径为空", label)
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("读取%s失败:%w", label, err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("%s不是普通文件", label)
}
}
return nil
}
func (s *Server) allowSetupRequest(c *gin.Context) bool {
s.configMu.RLock()
initialized := s.config.SetupReady
s.configMu.RUnlock()
if initialized {
s.writeError(c, http.StatusConflict, "初始化已经完成,不能重复执行初始化 DNS 测试", nil)
return false
}
if !isLoopbackRequest(c.Request) {
s.writeError(c, http.StatusForbidden, "首次初始化接口仅允许从本机访问", nil)
return false
}
return true
}
func isLoopbackRequest(request *http.Request) bool {
if request == nil {
return false
}
host, _, err := net.SplitHostPort(request.RemoteAddr)
if err != nil {
host = request.RemoteAddr
}
ip := net.ParseIP(strings.TrimSpace(host))
return ip != nil && ip.IsLoopback()
}
func newNotificationCredentialReference() (string, error) {
buffer := make([]byte, 18)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
return "secret://notification/" + base64.RawURLEncoding.EncodeToString(buffer), nil
}
func normalizeNotificationCredentials(authMode string, values map[string]string) (map[string]string, error) {
result := make(map[string]string, len(values))
for key, value := range values {
key = strings.TrimSpace(key)
if key == "" {
continue
}
result[key] = value
}
if len(result) == 0 {
return nil, fmt.Errorf("HTTP 通知凭据不能为空")
}
switch strings.ToLower(strings.TrimSpace(authMode)) {
case "bearer":
if strings.TrimSpace(result["token"]) == "" && strings.TrimSpace(result["access_token"]) == "" {
return nil, fmt.Errorf("Bearer Token 凭据必须包含 token 或 access_token")
}
case "hmac_sha256":
if strings.TrimSpace(result["secret"]) == "" && strings.TrimSpace(result["hmac_secret"]) == "" && strings.TrimSpace(result["key"]) == "" && strings.TrimSpace(result["token"]) == "" {
return nil, fmt.Errorf("HMAC-SHA256 凭据必须包含签名密钥")
}
case "session":
account := result["account"]
if account == "" {
account = result["username"]
}
if account == "" {
account = result["identifier"]
}
if strings.TrimSpace(account) == "" || result["password"] == "" {
return nil, fmt.Errorf("会话认证凭据必须包含账号和密码")
}
default:
return nil, fmt.Errorf("HTTP 通知认证方式无效")
}
return result, nil
}