diff --git a/agent/app/service/website_ca.go b/agent/app/service/website_ca.go index 6883b198de96..da05222722ec 100644 --- a/agent/app/service/website_ca.go +++ b/agent/app/service/website_ca.go @@ -188,7 +188,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...) } for _, domain := range existDomains { - if ipAddress := net.ParseIP(domain); ipAddress == nil { + if ipAddress := common.ParseIPLoose(domain); ipAddress == nil { domains = append(domains, domain) } else { ips = append(ips, ipAddress) @@ -220,7 +220,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website if req.Domains != "" { domainArray := strings.Split(req.Domains, "\n") for _, domain := range domainArray { - if ipAddress := net.ParseIP(domain); ipAddress == nil { + if ipAddress := common.ParseIPLoose(domain); ipAddress == nil { if domain != "localhost" && !common.IsValidDomain(domain) { err = buserr.WithName("ErrDomainFormat", domain) return nil, err diff --git a/agent/utils/common/common.go b/agent/utils/common/common.go index a49dda60574d..bd27a54dd73d 100644 --- a/agent/utils/common/common.go +++ b/agent/utils/common/common.go @@ -346,6 +346,26 @@ func IsValidIP(ip string) bool { return net.ParseIP(ip) != nil } +// ParseIPLoose parses an IP address, accepting bracketed IPv6 forms +// such as "[::1]" or "[fe80::1%eth0]" in addition to the bare forms +// that net.ParseIP supports natively. It also trims surrounding +// whitespace. Returns nil for non-IP input, matching net.ParseIP. +// +// This is required because some flows pass the host portion of a URL +// (e.g. "[::1]") rather than a bare IP address. Rejecting that form +// caused 1Panel-dev/1Panel#12646 — the panel SSL self-sign workflow +// reported “domain format invalid” for IPv6 hosts. +func ParseIPLoose(s string) net.IP { + trimmed := strings.TrimSpace(s) + if trimmed == "" { + return nil + } + if len(trimmed) >= 2 && trimmed[0] == '[' && trimmed[len(trimmed)-1] == ']' { + trimmed = trimmed[1 : len(trimmed)-1] + } + return net.ParseIP(trimmed) +} + const ( b = uint64(1) kb = 1024 * b diff --git a/agent/utils/common/parse_ip_test.go b/agent/utils/common/parse_ip_test.go new file mode 100644 index 000000000000..27fe2a58c8de --- /dev/null +++ b/agent/utils/common/parse_ip_test.go @@ -0,0 +1,36 @@ +package common + +import "testing" + +// Regression test for 1Panel-dev/1Panel#12646: panel SSL self-signed flow +// rejected IPv6 hosts because net.ParseIP does not accept the bracketed +// form (e.g. "[::1]") and the upstream caller passes the host portion of +// a URL rather than a bare IP. +func TestParseIPLoose(t *testing.T) { + cases := []struct { + name string + in string + want bool + }{ + {"bare ipv4", "127.0.0.1", true}, + {"bare ipv6", "::1", true}, + {"bracketed ipv6", "[::1]", true}, + {"bracketed full ipv6", "[2001:db8::1]", true}, + {"trimmed bare ipv6", " ::1 ", true}, + {"trimmed bracketed ipv6", " [::1] ", true}, + {"empty", "", false}, + {"only brackets", "[]", false}, + {"hostname", "example.com", false}, + {"bracketed garbage", "[notanip]", false}, + {"unbalanced bracket left", "[::1", false}, + {"unbalanced bracket right", "::1]", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := ParseIPLoose(tc.in) != nil + if got != tc.want { + t.Errorf("ParseIPLoose(%q) ok = %v, want %v", tc.in, got, tc.want) + } + }) + } +} diff --git a/frontend/src/views/setting/safe/ssl/index.vue b/frontend/src/views/setting/safe/ssl/index.vue index cc24efa74214..4966e7bb43ec 100644 --- a/frontend/src/views/setting/safe/ssl/index.vue +++ b/frontend/src/views/setting/safe/ssl/index.vue @@ -243,8 +243,11 @@ const onSaveSSL = async (formEl: FormInstance | undefined) => { cert: form.cert, key: form.key, }; - let href = window.location.href; - param.domain = href.split('//')[1].split(':')[0]; + // window.location.hostname yields the bare host name and strips + // surrounding brackets from IPv6 addresses (e.g. '[::1]' -> '::1'), + // unlike `href.split('//')[1].split(':')[0]` which incorrectly + // returns '[' for IPv6 URLs. See 1Panel-dev/1Panel#12646. + param.domain = window.location.hostname; await updateSSL(param).then(() => { MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); let href = window.location.href;