diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 675c0809..abb2ebb4 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -11,7 +11,7 @@ export function isValidIP(ip: string): boolean { export function isValidUrlGlobPattern(pattern: string): boolean { // Remove leading slash if present pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; - + // Empty string is not valid if (!pattern) { return false; @@ -19,11 +19,11 @@ export function isValidUrlGlobPattern(pattern: string): boolean { // Split path into segments const segments = pattern.split("/"); - + // Check each segment for (let i = 0; i < segments.length; i++) { const segment = segments[i]; - + // Empty segments are not allowed (double slashes), except at the end if (!segment && i !== segments.length - 1) { return false; @@ -37,12 +37,15 @@ export function isValidUrlGlobPattern(pattern: string): boolean { // Check each character in the segment for (let j = 0; j < segment.length; j++) { const char = segment[j]; - + // Check for percent-encoded sequences if (char === "%" && j + 2 < segment.length) { const hex1 = segment[j + 1]; const hex2 = segment[j + 2]; - if (!/^[0-9A-Fa-f]$/.test(hex1) || !/^[0-9A-Fa-f]$/.test(hex2)) { + if ( + !/^[0-9A-Fa-f]$/.test(hex1) || + !/^[0-9A-Fa-f]$/.test(hex2) + ) { return false; } j += 2; // Skip the next two characters @@ -58,6 +61,36 @@ export function isValidUrlGlobPattern(pattern: string): boolean { } } } - + return true; -} \ No newline at end of file +} + +export function isUrlValid(url: string | undefined) { + if (!url) return true; // the link is optional in the schema so if it's empty it's valid + var pattern = new RegExp( + "^(https?:\\/\\/)?" + // protocol + "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name + "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address + "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path + "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string + "(\\#[-a-z\\d_]*)?$", + "i" + ); + return !!pattern.test(url); +} + +export function isTargetValid(value: string | undefined) { + if (!value) return true; + + const DOMAIN_REGEX = + /^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/; + const IPV4_REGEX = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; + + if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { + return true; + } + + return DOMAIN_REGEX.test(value); +} diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 11f3de69..8d07e5d6 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -12,34 +12,7 @@ import { fromError } from "zod-validation-error"; import { addTargets } from "../newt/targets"; import { eq } from "drizzle-orm"; import { pickPort } from "./helpers"; - -// // Regular expressions for validation -// const DOMAIN_REGEX = -// /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; -// const IPV4_REGEX = -// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; -// const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; -// -// // Schema for domain names and IP addresses -// const domainSchema = z -// .string() -// .min(1, "Domain cannot be empty") -// .max(255, "Domain name too long") -// .refine( -// (value) => { -// // Check if it's a valid IP address (v4 or v6) -// if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { -// return true; -// } -// -// // Check if it's a valid domain name -// return DOMAIN_REGEX.test(value); -// }, -// { -// message: "Invalid domain name or IP address format", -// path: ["domain"] -// } -// ); +import { isTargetValid } from "@server/lib/validators"; const createTargetParamsSchema = z .object({ @@ -52,7 +25,7 @@ const createTargetParamsSchema = z const createTargetSchema = z .object({ - ip: z.string().min(1).max(255), + ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), port: z.number().int().min(1).max(65535), enabled: z.boolean().default(true) diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 4dbb2f45..45051e0a 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -11,34 +11,7 @@ import { fromError } from "zod-validation-error"; import { addPeer } from "../gerbil/peers"; import { addTargets } from "../newt/targets"; import { pickPort } from "./helpers"; - -// // Regular expressions for validation -// const DOMAIN_REGEX = -// /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; -// const IPV4_REGEX = -// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; -// const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; -// -// // Schema for domain names and IP addresses -// const domainSchema = z -// .string() -// .min(1, "Domain cannot be empty") -// .max(255, "Domain name too long") -// .refine( -// (value) => { -// // Check if it's a valid IP address (v4 or v6) -// if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { -// return true; -// } -// -// // Check if it's a valid domain name -// return DOMAIN_REGEX.test(value); -// }, -// { -// message: "Invalid domain name or IP address format", -// path: ["domain"] -// } -// ); +import { isTargetValid } from "@server/lib/validators"; const updateTargetParamsSchema = z .object({ @@ -48,7 +21,7 @@ const updateTargetParamsSchema = z const updateTargetBodySchema = z .object({ - ip: z.string().min(1).max(255), + ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), enabled: z.boolean().optional() diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index d912b505..c565b525 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -62,40 +62,11 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; -import { useSiteContext } from "@app/hooks/useSiteContext"; -import { InfoPopup } from "@app/components/ui/info-popup"; import { useRouter } from "next/navigation"; - -// Regular expressions for validation -const DOMAIN_REGEX = - /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; -const IPV4_REGEX = - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; -const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; - -// // Schema for domain names and IP addresses -// const domainSchema = z -// .string() -// .min(1, "Domain cannot be empty") -// .max(255, "Domain name too long") -// .refine( -// (value) => { -// // Check if it's a valid IP address (v4 or v6) -// if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) { -// return true; -// } -// -// // Check if it's a valid domain name -// return DOMAIN_REGEX.test(value); -// }, -// { -// message: "Invalid domain name or IP address format", -// path: ["domain"] -// } -// ); +import { isTargetValid } from "@server/lib/validators"; const addTargetSchema = z.object({ - ip: z.string().min(1).max(255), + ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive() // protocol: z.string(),