mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-07 11:16:37 +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 { build } from "@server/build";
|
||||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||||
import { DomainRow } from "../../../domains/DomainsTable";
|
import { DomainRow } from "../../../domains/DomainsTable";
|
||||||
import { toUnicode } from "punycode";
|
import { toASCII, toUnicode } from "punycode";
|
||||||
|
|
||||||
export default function GeneralForm() {
|
export default function GeneralForm() {
|
||||||
const [formKey, setFormKey] = useState(0);
|
const [formKey, setFormKey] = useState(0);
|
||||||
@@ -82,7 +82,7 @@ export default function GeneralForm() {
|
|||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
const [resourceFullDomain, setResourceFullDomain] = useState(
|
const [resourceFullDomain, setResourceFullDomain] = useState(
|
||||||
`${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
||||||
);
|
);
|
||||||
const [selectedDomain, setSelectedDomain] = useState<{
|
const [selectedDomain, setSelectedDomain] = useState<{
|
||||||
domainId: string;
|
domainId: string;
|
||||||
@@ -186,7 +186,7 @@ export default function GeneralForm() {
|
|||||||
{
|
{
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
|
||||||
domainId: data.domainId,
|
domainId: data.domainId,
|
||||||
proxyPort: data.proxyPort,
|
proxyPort: data.proxyPort,
|
||||||
// ...(!resource.http && {
|
// ...(!resource.http && {
|
||||||
@@ -478,7 +478,6 @@ export default function GeneralForm() {
|
|||||||
setEditDomainOpen(false);
|
setEditDomainOpen(false);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Domain sanitized",
|
|
||||||
description: `Final domain: ${sanitizedFullDomain}`,
|
description: `Final domain: ${sanitizedFullDomain}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import { isTargetValid } from "@server/lib/validators";
|
|||||||
import { ListTargetsResponse } from "@server/routers/target";
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||||
import { toUnicode } from 'punycode';
|
import { toASCII, toUnicode } from 'punycode';
|
||||||
import { DomainRow } from "../../domains/DomainsTable";
|
import { DomainRow } from "../../domains/DomainsTable";
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
@@ -329,7 +329,7 @@ export default function Page() {
|
|||||||
if (isHttp) {
|
if (isHttp) {
|
||||||
const httpData = httpForm.getValues();
|
const httpData = httpForm.getValues();
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
subdomain: httpData.subdomain,
|
subdomain: httpData.subdomain ? toASCII(httpData.subdomain) : undefined,
|
||||||
domainId: httpData.domainId,
|
domainId: httpData.domainId,
|
||||||
protocol: "tcp"
|
protocol: "tcp"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -336,8 +336,13 @@ export default function DomainPicker2({
|
|||||||
const handleBaseDomainSelect = (option: DomainOption) => {
|
const handleBaseDomainSelect = (option: DomainOption) => {
|
||||||
let sub = subdomainInput;
|
let sub = subdomainInput;
|
||||||
|
|
||||||
sub = finalizeSubdomain(sub, option);
|
if (sub && sub.trim() !== "") {
|
||||||
setSubdomainInput(sub);
|
sub = finalizeSubdomain(sub, option) || "";
|
||||||
|
setSubdomainInput(sub);
|
||||||
|
} else {
|
||||||
|
sub = "";
|
||||||
|
setSubdomainInput("");
|
||||||
|
}
|
||||||
|
|
||||||
if (option.type === "provided-search") {
|
if (option.type === "provided-search") {
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
@@ -409,11 +414,6 @@ export default function DomainPicker2({
|
|||||||
sortedAvailableOptions.length > providedDomainsShown;
|
sortedAvailableOptions.length > providedDomainsShown;
|
||||||
|
|
||||||
|
|
||||||
const isValidDomainCharacter = (char: string) => {
|
|
||||||
// Allow Unicode letters, numbers, hyphens, and periods
|
|
||||||
return /[\p{L}\p{N}.-]/u.test(char);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-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"
|
"border-red-500 focus:border-red-500"
|
||||||
)}
|
)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const rawInput = e.target.value;
|
|
||||||
const validInput = rawInput
|
|
||||||
.split("")
|
|
||||||
.filter((char) => isValidDomainCharacter(char))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
if (showProvidedDomainSearch) {
|
if (showProvidedDomainSearch) {
|
||||||
handleProvidedDomainInputChange(validInput);
|
handleProvidedDomainInputChange(e.target.value);
|
||||||
} else {
|
} else {
|
||||||
handleSubdomainChange(validInput);
|
handleSubdomainChange(e.target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
|
|
||||||
export type DomainType = "organization" | "provided" | "provided-search";
|
export type DomainType = "organization" | "provided" | "provided-search";
|
||||||
|
|
||||||
export const SINGLE_LABEL_RE = /^[a-z0-9-]+$/i; // provided-search (no dots)
|
export const SINGLE_LABEL_RE = /^[\p{L}\p{N}-]+$/u; // provided-search (no dots)
|
||||||
export const MULTI_LABEL_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/i; // ns/wildcard
|
export const MULTI_LABEL_RE = /^[\p{L}\p{N}-]+(\.[\p{L}\p{N}-]+)*$/u; // 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_STRICT_RE = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u; // start/end alnum
|
||||||
|
|
||||||
|
|
||||||
export function sanitizeInputRaw(input: string): string {
|
export function sanitizeInputRaw(input: string): string {
|
||||||
if (!input) return "";
|
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 {
|
export function finalizeSubdomainSanitize(input: string): string {
|
||||||
if (!input) return "";
|
if (!input) return "";
|
||||||
return input
|
return input
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9.-]/g, "") // allow only valid chars
|
.normalize("NFC")
|
||||||
.replace(/\.{2,}/g, ".") // collapse multiple dots
|
.replace(/[^\p{L}\p{N}.-]/gu, "") // allow Unicode
|
||||||
.replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
|
.replace(/\.{2,}/g, ".") // collapse multiple dots
|
||||||
.replace(/^\.+|\.+$/g, ""); // strip leading/trailing 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 {
|
export function validateByDomainType(subdomain: string, domainType: { type: "provided-search" | "organization"; domainType?: "ns" | "cname" | "wildcard" } ): boolean {
|
||||||
if (!domainType) return false;
|
if (!domainType) return false;
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export function validateByDomainType(subdomain: string, domainType: { type: "pro
|
|||||||
|
|
||||||
|
|
||||||
export const isValidSubdomainStructure = (input: string): boolean => {
|
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) return false;
|
||||||
if (input.includes("..")) return false;
|
if (input.includes("..")) return false;
|
||||||
@@ -57,3 +60,4 @@ export const isValidSubdomainStructure = (input: string): boolean => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user