mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Support unicode with subdomain sanitized
This commit is contained in:
@@ -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}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user