From 65e27501022dc9a2ca3eba60fdcf61babbf0aabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 3 Jul 2026 01:21:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(website):=20=E4=BF=AE=E5=A4=8D=20IP=20?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=9F=9F=E5=90=8D=E7=9A=84=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=88=E5=B0=A4=E5=85=B6=20IPv6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取 IsIPv6/WrapIPv6/UnwrapIPv6 到 pkg/tools,复用于 acme panelSolver - server_name 写入时为 IPv6 套方括号、读取时剥离,Domains 统一裸地址口径,证书 SAN 判定自然正确 - 前端打开网站链接时为 IPv6 套方括号,避免拼出非法 URL Co-Authored-By: Claude Fable 5 --- internal/data/website.go | 4 ++++ pkg/acme/solvers.go | 20 ++++---------------- pkg/tools/tools.go | 26 ++++++++++++++++++++++++++ web/src/utils/common/common.ts | 5 +++++ web/src/views/website/ListView.vue | 6 +++--- 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/internal/data/website.go b/internal/data/website.go index ca594115..99c3462f 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -28,6 +28,7 @@ import ( "github.com/acepanel/panel/v3/pkg/punycode" "github.com/acepanel/panel/v3/pkg/shell" "github.com/acepanel/panel/v3/pkg/systemctl" + "github.com/acepanel/panel/v3/pkg/tools" "github.com/acepanel/panel/v3/pkg/types" "github.com/acepanel/panel/v3/pkg/webserver" webservertypes "github.com/acepanel/panel/v3/pkg/webserver/types" @@ -147,6 +148,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { setting.Listens = vhost.Listen() // 域名 domains := vhost.ServerName() + domains = lo.Map(domains, func(d string, _ int) string { return tools.UnwrapIPv6(d) }) domains, err = punycode.DecodeDomains(domains) if err != nil { return nil, err @@ -322,6 +324,7 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (* if err != nil { return nil, err } + domains = lo.Map(domains, func(d string, _ int) string { return tools.WrapIPv6(d) }) if err = vhost.SetServerName(domains); err != nil { return nil, err } @@ -604,6 +607,7 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er if err != nil { return err } + domains = lo.Map(domains, func(d string, _ int) string { return tools.WrapIPv6(d) }) if err = vhost.SetServerName(domains); err != nil { return err } diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index 3e94444b..97b10dad 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -6,7 +6,6 @@ import ( "fmt" "net" "net/http" - "net/netip" "os" "path/filepath" "strings" @@ -30,6 +29,7 @@ import ( pkgos "github.com/acepanel/panel/v3/pkg/os" "github.com/acepanel/panel/v3/pkg/shell" "github.com/acepanel/panel/v3/pkg/systemctl" + "github.com/acepanel/panel/v3/pkg/tools" ) var panelSolverGlobal sync.Mutex @@ -116,7 +116,7 @@ func (s *panelSolver) startServer() error { } func (s *panelSolver) writeNginxConfig() error { - hasIPv6 := lo.SomeBy(s.ip, s.isIPv6) + hasIPv6 := lo.SomeBy(s.ip, tools.IsIPv6) var conf strings.Builder conf.WriteString("server {\n listen 80;\n") @@ -125,10 +125,7 @@ func (s *panelSolver) writeNginxConfig() error { conf.WriteString(" listen [::]:80;\n") } names := lo.Map(s.ip, func(ip string, _ int) string { - if s.isIPv6(ip) { - return "[" + ip + "]" - } - return ip + return tools.WrapIPv6(ip) }) _, _ = fmt.Fprintf(&conf, " server_name %s;\n", strings.Join(names, " ")) for path, token := range s.tokens { @@ -166,10 +163,7 @@ func (s *panelSolver) writeApacheConfig() error { var conf strings.Builder addrs := lo.Map(s.ip, func(ip string, _ int) string { - if s.isIPv6(ip) { - return "[" + ip + "]:80" - } - return ip + ":80" + return tools.WrapIPv6(ip) + ":80" }) _, _ = fmt.Fprintf(&conf, "\n", strings.Join(addrs, " ")) conf.WriteString(" ServerName acme-ip-validation\n") @@ -236,12 +230,6 @@ func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error { return nil } -// isIPv6 判断 host 是否为 IPv6 地址 -func (s *panelSolver) isIPv6(host string) bool { - addr, err := netip.ParseAddr(host) - return err == nil && !addr.Is4() -} - type httpSolver struct { conf string webServer string // "nginx" or "apache" diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 20b5dd4d..50b06259 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -8,6 +8,7 @@ import ( "fmt" stdnet "net" "net/http" + "net/netip" "slices" "sort" "strings" @@ -313,3 +314,28 @@ func FormatBytes(size float64) string { return fmt.Sprintf("%.2f %s", size, units[i]) } + +// IsIPv6 判断 host 是否为 IPv6 地址(裸地址,不含方括号) +func IsIPv6(host string) bool { + addr, err := netip.ParseAddr(host) + return err == nil && !addr.Is4() +} + +// WrapIPv6 为裸 IPv6 地址套上方括号(如 ::1 → [::1]),用于 nginx server_name、URL 等语境 +// 非 IPv6 或已带方括号则原样返回 +func WrapIPv6(host string) string { + if IsIPv6(host) { + return "[" + host + "]" + } + return host +} + +// UnwrapIPv6 去除 IPv6 地址的方括号(如 [::1] → ::1),非此形式则原样返回 +func UnwrapIPv6(host string) string { + if inner, ok := strings.CutPrefix(host, "["); ok { + if inner, ok = strings.CutSuffix(inner, "]"); ok && IsIPv6(inner) { + return inner + } + } + return host +} diff --git a/web/src/utils/common/common.ts b/web/src/utils/common/common.ts index 86883255..b29a085b 100644 --- a/web/src/utils/common/common.ts +++ b/web/src/utils/common/common.ts @@ -56,3 +56,8 @@ export function generateRandomString(length: number) { } return result } + +/** 为裸 IPv6 地址套上方括号(如 ::1 → [::1]),用于拼接 URL;非 IPv6 或已带方括号则原样返回 */ +export function wrapIPv6(host: string): string { + return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host +} diff --git a/web/src/views/website/ListView.vue b/web/src/views/website/ListView.vue index ff70fb87..b51aba3e 100644 --- a/web/src/views/website/ListView.vue +++ b/web/src/views/website/ListView.vue @@ -17,7 +17,7 @@ import website from '@/api/panel/website' import TheIcon from '@/components/custom/TheIcon.vue' import ConfirmDialog from '@/components/system/ConfirmDialog.vue' import { useFileStore } from '@/stores' -import { isNullOrUndef } from '@/utils' +import { isNullOrUndef, wrapIPv6 } from '@/utils' const type = defineModel('type', { type: String, required: true }) // 网站类型 const createModal = defineModel('createModal', { type: Boolean, required: true }) // 创建网站 @@ -63,7 +63,7 @@ const columns: any = [ class: 'cursor-pointer hover:opacity-60 inline-flex', onDblclick: () => { const protocol = row.ssl ? 'https' : 'http' - window.open(`${protocol}://${row.domains[0]}`, '_blank') + window.open(`${protocol}://${wrapIPv6(row.domains[0])}`, '_blank') }, }, [h(TheIcon, { icon: 'mdi:link-variant', size: 16 })], @@ -76,7 +76,7 @@ const columns: any = [ default: () => row.domains.map((domain: string) => { const protocol = row.ssl ? 'https' : 'http' - const url = `${protocol}://${domain}` + const url = `${protocol}://${wrapIPv6(domain)}` return h( NFlex, { align: 'center', size: 'small' },