统一多级域名泛域名生成与预览
集中处理泛域名识别、生成、编辑回显和多级子域名预览,补充前后端测试覆盖。
This commit is contained in:
parent
79c5fe112c
commit
73511d6fcb
@ -75,10 +75,10 @@ func NewDomainWithOptions(name string, certificateType CertificateType, sans []s
|
||||
if certificateType != CertificateSAN {
|
||||
cleanSANs = []string{name}
|
||||
}
|
||||
if certificateType == CertificateWildcard && !strings.HasPrefix(name, "*.") {
|
||||
if certificateType == CertificateWildcard && !IsWildcardName(name) {
|
||||
return Domain{}, fmt.Errorf("泛域名必须以 *. 开头")
|
||||
}
|
||||
if certificateType != CertificateWildcard && strings.HasPrefix(name, "*.") {
|
||||
if certificateType != CertificateWildcard && IsWildcardName(name) {
|
||||
return Domain{}, fmt.Errorf("只有泛域名证书类型允许使用 *. 域名")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
@ -104,12 +104,24 @@ func IsValidACMEEnvironment(value string) bool {
|
||||
|
||||
func IsValidName(name string) bool {
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
if strings.HasPrefix(name, "*.") {
|
||||
if IsWildcardName(name) {
|
||||
name = strings.TrimPrefix(name, "*.")
|
||||
}
|
||||
return domainPattern.MatchString(name)
|
||||
}
|
||||
|
||||
func IsWildcardName(name string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(strings.ToLower(name)), "*.")
|
||||
}
|
||||
|
||||
func WildcardName(name string) (string, error) {
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
if IsWildcardName(name) || !IsValidName(name) {
|
||||
return "", fmt.Errorf("主域名格式无效,无法生成泛域名:%s", name)
|
||||
}
|
||||
return "*." + name, nil
|
||||
}
|
||||
|
||||
func normalizeSANs(sans []string) []string {
|
||||
seen := make(map[string]struct{}, len(sans))
|
||||
result := make([]string, 0, len(sans))
|
||||
|
||||
@ -81,3 +81,26 @@ func TestNewDomainOptionsValidation(t *testing.T) {
|
||||
t.Fatal("非法 ACME 环境未被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardNamePreservesMultiLevelDomain(t *testing.T) {
|
||||
for _, item := range []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{input: "x.x.com", want: "*.x.x.com"},
|
||||
{input: " X.X.X.COM ", want: "*.x.x.x.com"},
|
||||
} {
|
||||
got, err := WildcardName(item.input)
|
||||
if err != nil {
|
||||
t.Fatalf("WildcardName(%q) failed: %v", item.input, err)
|
||||
}
|
||||
if got != item.want {
|
||||
t.Fatalf("WildcardName(%q) = %q, want %q", item.input, got, item.want)
|
||||
}
|
||||
}
|
||||
for _, input := range []string{"*.x.x.com", "bad_domain"} {
|
||||
if _, err := WildcardName(input); err == nil {
|
||||
t.Fatalf("WildcardName(%q) should reject invalid base domain", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -761,13 +761,16 @@ func newDomainFromRequest(request domainRequest) (domain.Domain, error) {
|
||||
sans := append([]string(nil), request.SANs...)
|
||||
if request.IncludeWildcard {
|
||||
name := strings.TrimSpace(request.Name)
|
||||
if strings.HasPrefix(name, "*.") {
|
||||
if domain.IsWildcardName(name) {
|
||||
return domain.Domain{}, errors.New("同时申请泛域名时,主域名不能以 *. 开头")
|
||||
}
|
||||
if certificateType != domain.CertificateSingle {
|
||||
return domain.Domain{}, errors.New("同时申请泛域名仅适用于单域名证书")
|
||||
}
|
||||
wildcardName := "*." + strings.ToLower(name)
|
||||
wildcardName, err := domain.WildcardName(name)
|
||||
if err != nil {
|
||||
return domain.Domain{}, err
|
||||
}
|
||||
sans = append(sans, wildcardName)
|
||||
certificateType = domain.CertificateSAN
|
||||
}
|
||||
|
||||
@ -1,6 +1,19 @@
|
||||
import { computed, onBeforeUnmount, ref } from "vue";
|
||||
import { useDomainDNSAccount } from "./DomainDNSAccount.js";
|
||||
|
||||
function normalizeDomainName(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isWildcardName(value) {
|
||||
return normalizeDomainName(value).startsWith("*.");
|
||||
}
|
||||
|
||||
function wildcardNameFor(value) {
|
||||
const name = normalizeDomainName(value);
|
||||
return name && !isWildcardName(name) ? `*.${name}` : "";
|
||||
}
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
project_id: "default",
|
||||
@ -17,10 +30,10 @@ function emptyForm() {
|
||||
}
|
||||
|
||||
function domainPayload(form) {
|
||||
const name = form.name.trim();
|
||||
const name = normalizeDomainName(form.name);
|
||||
const includeWildcard = form.certificate_type === "single"
|
||||
&& Boolean(form.include_wildcard)
|
||||
&& !name.startsWith("*.");
|
||||
&& !isWildcardName(name);
|
||||
return {
|
||||
name: form.name,
|
||||
certificate_type: form.certificate_type,
|
||||
@ -44,7 +57,8 @@ export function useDomainForm({ fetchAdmin, domains, servers, domainServers, not
|
||||
const domainBindingsReady = ref(true);
|
||||
const domainSaveUncertain = ref(false);
|
||||
const isEditing = computed(() => Boolean(editingId.value));
|
||||
const formTitle = computed(() => isEditing.value ? "编辑域名配置" : "添加托管域名");
|
||||
const formTitle = computed(() => isEditing.value ? '编辑域名配置' : '添加托管域名');
|
||||
const wildcardName = computed(() => wildcardNameFor(form.value.name));
|
||||
const domainServerOptions = computed(() => servers.value.filter(
|
||||
(server) => Boolean(form.value.project_id) && server.project_id === form.value.project_id,
|
||||
));
|
||||
@ -76,11 +90,11 @@ export function useDomainForm({ fetchAdmin, domains, servers, domainServers, not
|
||||
version += 1;
|
||||
editingId.value = item.id;
|
||||
targetID = item.id;
|
||||
const itemSANs = (item.sans || []).filter((name) => name !== item.name);
|
||||
const normalizedName = (item.name || "").trim().toLowerCase();
|
||||
const normalizedName = normalizeDomainName(item.name);
|
||||
const itemSANs = (item.sans || []).filter((name) => normalizeDomainName(name) !== normalizedName);
|
||||
const combinedWildcard = item.certificate_type === "san"
|
||||
&& itemSANs.length === 1
|
||||
&& itemSANs[0].trim().toLowerCase() === `*.${normalizedName}`;
|
||||
&& normalizeDomainName(itemSANs[0]) === `*.${normalizedName}`;
|
||||
form.value = {
|
||||
...emptyForm(),
|
||||
project_id: item.project_id || "",
|
||||
@ -168,7 +182,7 @@ export function useDomainForm({ fetchAdmin, domains, servers, domainServers, not
|
||||
if (payload.challenge_type === "http-01" && (
|
||||
payload.certificate_type === "wildcard" ||
|
||||
payload.include_wildcard ||
|
||||
[payload.name, ...payload.sans].some((name) => name.trim().startsWith("*."))
|
||||
[payload.name, ...payload.sans].some((name) => isWildcardName(name))
|
||||
)) {
|
||||
formError.value = "HTTP-01 不支持泛域名,请选择 DNS-01";
|
||||
return;
|
||||
@ -177,11 +191,11 @@ export function useDomainForm({ fetchAdmin, domains, servers, domainServers, not
|
||||
formError.value = "请输入主域名";
|
||||
return;
|
||||
}
|
||||
if (payload.certificate_type === "wildcard" && !payload.name.trim().startsWith("*.")) {
|
||||
if (payload.certificate_type === "wildcard" && !isWildcardName(payload.name)) {
|
||||
formError.value = "泛域名主域名必须以 *. 开头,例如 *.example.com";
|
||||
return;
|
||||
}
|
||||
if (payload.certificate_type !== "wildcard" && payload.name.trim().startsWith("*.")) {
|
||||
if (payload.certificate_type !== "wildcard" && isWildcardName(payload.name)) {
|
||||
formError.value = "只有泛域名证书类型允许使用 *. 域名";
|
||||
return;
|
||||
}
|
||||
@ -317,7 +331,7 @@ export function useDomainForm({ fetchAdmin, domains, servers, domainServers, not
|
||||
dnsUnavailableSelection: dnsAccount.dnsUnavailableSelection,
|
||||
reloadDomainDNS: dnsAccount.reloadDomainDNS,
|
||||
form, editingId, saving, showForm, formError, isEditing, formTitle,
|
||||
domainServerOptions, domainBindingsLoading, domainBindingsReady, domainSaveUncertain,
|
||||
domainServerOptions, domainBindingsLoading, domainBindingsReady, domainSaveUncertain, wildcardName,
|
||||
openCreate, openEdit, closeForm, saveDomain, reloadDomainBindings,
|
||||
onCertificateTypeChange, onIncludeWildcardChange,
|
||||
};
|
||||
|
||||
@ -316,3 +316,29 @@ test("关联网络失败可重试,但刷新失败不能被误报为保存失
|
||||
assert.equal(state.formError.value, "");
|
||||
assert.match(deps.notice.value, /配置已保存.*列表刷新失败/);
|
||||
});
|
||||
|
||||
test("多级域名泛域名预览统一使用规范化后的完整名称", async (t) => {
|
||||
const { state } = await mountForm(t, async () => Response.json({ items: [] }));
|
||||
await state.openCreate();
|
||||
state.form.value.name = " X.X.X.COM ";
|
||||
assert.equal(state.wildcardName.value, "*.x.x.x.com");
|
||||
state.form.value.include_wildcard = true;
|
||||
assert.equal(state.wildcardName.value, "*.x.x.x.com");
|
||||
});
|
||||
|
||||
test("编辑联合申请时忽略大小写差异并正确识别多级泛域名", async (t) => {
|
||||
const { state } = await mountForm(t, async () => Response.json({ items: [] }));
|
||||
await state.openEdit({
|
||||
id: "combined-domain",
|
||||
project_id: "default",
|
||||
name: "X.X.X.COM",
|
||||
certificate_type: "san",
|
||||
sans: ["x.x.x.com", "*.X.X.X.COM"],
|
||||
auto_renew: true,
|
||||
acme_environment: "staging",
|
||||
challenge_type: "dns-01",
|
||||
});
|
||||
assert.equal(state.form.value.certificate_type, "single");
|
||||
assert.equal(state.form.value.include_wildcard, true);
|
||||
assert.equal(state.form.value.sans, "");
|
||||
});
|
||||
@ -51,6 +51,7 @@ async function renderModal(overrides = {}) {
|
||||
webServerLabels: {},
|
||||
formError: "",
|
||||
saving: false,
|
||||
wildcardName: "",
|
||||
domainBindingsLoading: false,
|
||||
domainBindingsReady: true,
|
||||
domainSaveUncertain: false,
|
||||
|
||||
@ -26,6 +26,7 @@ const {
|
||||
webServerLabels,
|
||||
formError,
|
||||
saving,
|
||||
wildcardName = "",
|
||||
onCertificateTypeChange = () => {},
|
||||
onIncludeWildcardChange = () => {},
|
||||
} = props.state;
|
||||
@ -85,7 +86,7 @@ const {
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
v-if="form.certificate_type === 'single' && !form.name.trim().startsWith('*.')"
|
||||
v-if="form.certificate_type === 'single' && !form.name.trim().toLowerCase().startsWith('*.')"
|
||||
class="switch-field domain-wildcard-option"
|
||||
for="include-wildcard"
|
||||
>
|
||||
@ -99,11 +100,11 @@ const {
|
||||
/>
|
||||
<span>
|
||||
<strong>同时申请泛域名</strong>
|
||||
<small>将按当前完整主域名同时申请 {{ form.name || "example.com" }} 和 *.{{ form.name || "example.com" }},多级子域名也只向前增加一级通配符。</small>
|
||||
<small>将按当前完整主域名同时申请 {{ form.name.trim().toLowerCase() || "example.com" }} 和 *.{{ form.name.trim().toLowerCase() || "example.com" }},多级子域名也只向前增加一级通配符。</small>
|
||||
</span>
|
||||
</label>
|
||||
<p v-if="form.include_wildcard" class="field-hint domain-wildcard-hint">
|
||||
本次将申请:{{ form.name }}、*.{{ form.name }};泛域名只能使用 DNS-01 验证。
|
||||
本次将申请:{{ form.name.trim().toLowerCase() }}、{{ wildcardName || ("*." + form.name.trim().toLowerCase()) }};泛域名只能使用 DNS-01 验证。
|
||||
</p>
|
||||
<label
|
||||
v-if="form.certificate_type === 'san'"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user