mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
fix(website): 修复 IP 作为域名的处理(尤其 IPv6)
- 抽取 IsIPv6/WrapIPv6/UnwrapIPv6 到 pkg/tools,复用于 acme panelSolver - server_name 写入时为 IPv6 套方括号、读取时剥离,Domains 统一裸地址口径,证书 SAN 判定自然正确 - 前端打开网站链接时为 IPv6 套方括号,避免拼出非法 URL Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
+4
-16
@@ -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, "<VirtualHost %s>\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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string>('type', { type: String, required: true }) // 网站类型
|
||||
const createModal = defineModel<boolean>('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' },
|
||||
|
||||
Reference in New Issue
Block a user