Support unicode with subdomain sanitized

This commit is contained in:
Pallavi
2025-08-31 22:45:42 +05:30
parent 8a62f12e8b
commit 7d5961cf50
4 changed files with 29 additions and 32 deletions

View File

@@ -55,7 +55,7 @@ import { Globe } from "lucide-react";
import { build } from "@server/build";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { DomainRow } from "../../../domains/DomainsTable";
import { toUnicode } from "punycode";
import { toASCII, toUnicode } from "punycode";
export default function GeneralForm() {
const [formKey, setFormKey] = useState(0);
@@ -82,7 +82,7 @@ export default function GeneralForm() {
const [loadingPage, setLoadingPage] = useState(true);
const [resourceFullDomain, setResourceFullDomain] = useState(
`${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
);
const [selectedDomain, setSelectedDomain] = useState<{
domainId: string;
@@ -186,7 +186,7 @@ export default function GeneralForm() {
{
enabled: data.enabled,
name: data.name,
subdomain: data.subdomain,
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
domainId: data.domainId,
proxyPort: data.proxyPort,
// ...(!resource.http && {
@@ -478,7 +478,6 @@ export default function GeneralForm() {
setEditDomainOpen(false);
toast({
title: "Domain sanitized",
description: `Final domain: ${sanitizedFullDomain}`,
});
}

View File

@@ -89,7 +89,7 @@ import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { toUnicode } from 'punycode';
import { toASCII, toUnicode } from 'punycode';
import { DomainRow } from "../../domains/DomainsTable";
const baseResourceFormSchema = z.object({
@@ -329,7 +329,7 @@ export default function Page() {
if (isHttp) {
const httpData = httpForm.getValues();
Object.assign(payload, {
subdomain: httpData.subdomain,
subdomain: httpData.subdomain ? toASCII(httpData.subdomain) : undefined,
domainId: httpData.domainId,
protocol: "tcp"
});

View File

@@ -336,8 +336,13 @@ export default function DomainPicker2({
const handleBaseDomainSelect = (option: DomainOption) => {
let sub = subdomainInput;
sub = finalizeSubdomain(sub, option);
setSubdomainInput(sub);
if (sub && sub.trim() !== "") {
sub = finalizeSubdomain(sub, option) || "";
setSubdomainInput(sub);
} else {
sub = "";
setSubdomainInput("");
}
if (option.type === "provided-search") {
setUserInput("");
@@ -409,11 +414,6 @@ export default function DomainPicker2({
sortedAvailableOptions.length > providedDomainsShown;
const isValidDomainCharacter = (char: string) => {
// Allow Unicode letters, numbers, hyphens, and periods
return /[\p{L}\p{N}.-]/u.test(char);
};
return (
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -444,16 +444,10 @@ export default function DomainPicker2({
"border-red-500 focus:border-red-500"
)}
onChange={(e) => {
const rawInput = e.target.value;
const validInput = rawInput
.split("")
.filter((char) => isValidDomainCharacter(char))
.join("");
if (showProvidedDomainSearch) {
handleProvidedDomainInputChange(validInput);
handleProvidedDomainInputChange(e.target.value);
} else {
handleSubdomainChange(validInput);
handleSubdomainChange(e.target.value);
}
}}
/>

View File

@@ -1,29 +1,32 @@
export type DomainType = "organization" | "provided" | "provided-search";
export const SINGLE_LABEL_RE = /^[a-z0-9-]+$/i; // provided-search (no dots)
export const MULTI_LABEL_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/i; // ns/wildcard
export const SINGLE_LABEL_STRICT_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; // start/end alnum
export const SINGLE_LABEL_RE = /^[\p{L}\p{N}-]+$/u; // provided-search (no dots)
export const MULTI_LABEL_RE = /^[\p{L}\p{N}-]+(\.[\p{L}\p{N}-]+)*$/u; // ns/wildcard
export const SINGLE_LABEL_STRICT_RE = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u; // start/end alnum
export function sanitizeInputRaw(input: string): string {
if (!input) return "";
return input.toLowerCase().replace(/[^a-z0-9.-]/g, "");
return input
.toLowerCase()
.normalize("NFC") // normalize Unicode
.replace(/[^\p{L}\p{N}.-]/gu, ""); // allow Unicode letters, numbers, dot, hyphen
}
export function finalizeSubdomainSanitize(input: string): string {
if (!input) return "";
return input
.toLowerCase()
.replace(/[^a-z0-9.-]/g, "") // allow only valid chars
.replace(/\.{2,}/g, ".") // collapse multiple dots
.replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
.replace(/^\.+|\.+$/g, ""); // strip leading/trailing dots
.normalize("NFC")
.replace(/[^\p{L}\p{N}.-]/gu, "") // allow Unicode
.replace(/\.{2,}/g, ".") // collapse multiple dots
.replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
.replace(/^\.+|\.+$/g, "") // strip leading/trailing dots
.replace(/(\.-)|(-\.)/g, "."); // fix illegal dot-hyphen combos
}
export function validateByDomainType(subdomain: string, domainType: { type: "provided-search" | "organization"; domainType?: "ns" | "cname" | "wildcard" } ): boolean {
if (!domainType) return false;
@@ -47,7 +50,7 @@ export function validateByDomainType(subdomain: string, domainType: { type: "pro
export const isValidSubdomainStructure = (input: string): boolean => {
const regex = /^(?!-)([a-zA-Z0-9-]{1,63})(?<!-)$/;
const regex = /^(?!-)([\p{L}\p{N}-]{1,63})(?<!-)$/u;
if (!input) return false;
if (input.includes("..")) return false;
@@ -57,3 +60,4 @@ export const isValidSubdomainStructure = (input: string): boolean => {