create target validator and add url validator

This commit is contained in:
Milo Schwartz 2025-02-14 16:46:46 -05:00
parent a418195b28
commit d5a220a004
No known key found for this signature in database
4 changed files with 46 additions and 96 deletions

View file

@ -11,7 +11,7 @@ export function isValidIP(ip: string): boolean {
export function isValidUrlGlobPattern(pattern: string): boolean { export function isValidUrlGlobPattern(pattern: string): boolean {
// Remove leading slash if present // Remove leading slash if present
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
// Empty string is not valid // Empty string is not valid
if (!pattern) { if (!pattern) {
return false; return false;
@ -19,11 +19,11 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
// Split path into segments // Split path into segments
const segments = pattern.split("/"); const segments = pattern.split("/");
// Check each segment // Check each segment
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i]; const segment = segments[i];
// Empty segments are not allowed (double slashes), except at the end // Empty segments are not allowed (double slashes), except at the end
if (!segment && i !== segments.length - 1) { if (!segment && i !== segments.length - 1) {
return false; return false;
@ -37,12 +37,15 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
// Check each character in the segment // Check each character in the segment
for (let j = 0; j < segment.length; j++) { for (let j = 0; j < segment.length; j++) {
const char = segment[j]; const char = segment[j];
// Check for percent-encoded sequences // Check for percent-encoded sequences
if (char === "%" && j + 2 < segment.length) { if (char === "%" && j + 2 < segment.length) {
const hex1 = segment[j + 1]; const hex1 = segment[j + 1];
const hex2 = segment[j + 2]; 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; return false;
} }
j += 2; // Skip the next two characters j += 2; // Skip the next two characters
@ -58,6 +61,36 @@ export function isValidUrlGlobPattern(pattern: string): boolean {
} }
} }
} }
return true; return true;
} }
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);
}

View file

@ -12,34 +12,7 @@ import { fromError } from "zod-validation-error";
import { addTargets } from "../newt/targets"; import { addTargets } from "../newt/targets";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { pickPort } from "./helpers"; import { pickPort } from "./helpers";
import { isTargetValid } from "@server/lib/validators";
// // 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"]
// }
// );
const createTargetParamsSchema = z const createTargetParamsSchema = z
.object({ .object({
@ -52,7 +25,7 @@ const createTargetParamsSchema = z
const createTargetSchema = z const createTargetSchema = z
.object({ .object({
ip: z.string().min(1).max(255), ip: z.string().refine(isTargetValid),
method: z.string().optional().nullable(), method: z.string().optional().nullable(),
port: z.number().int().min(1).max(65535), port: z.number().int().min(1).max(65535),
enabled: z.boolean().default(true) enabled: z.boolean().default(true)

View file

@ -11,34 +11,7 @@ import { fromError } from "zod-validation-error";
import { addPeer } from "../gerbil/peers"; import { addPeer } from "../gerbil/peers";
import { addTargets } from "../newt/targets"; import { addTargets } from "../newt/targets";
import { pickPort } from "./helpers"; import { pickPort } from "./helpers";
import { isTargetValid } from "@server/lib/validators";
// // 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"]
// }
// );
const updateTargetParamsSchema = z const updateTargetParamsSchema = z
.object({ .object({
@ -48,7 +21,7 @@ const updateTargetParamsSchema = z
const updateTargetBodySchema = z const updateTargetBodySchema = z
.object({ .object({
ip: z.string().min(1).max(255), ip: z.string().refine(isTargetValid),
method: z.string().min(1).max(10).optional().nullable(), method: z.string().min(1).max(10).optional().nullable(),
port: z.number().int().min(1).max(65535).optional(), port: z.number().int().min(1).max(65535).optional(),
enabled: z.boolean().optional() enabled: z.boolean().optional()

View file

@ -62,40 +62,11 @@ import {
SettingsSectionFooter SettingsSectionFooter
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; 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"; import { useRouter } from "next/navigation";
import { isTargetValid } from "@server/lib/validators";
// 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"]
// }
// );
const addTargetSchema = z.object({ const addTargetSchema = z.object({
ip: z.string().min(1).max(255), ip: z.string().refine(isTargetValid),
method: z.string().nullable(), method: z.string().nullable(),
port: z.coerce.number().int().positive() port: z.coerce.number().int().positive()
// protocol: z.string(), // protocol: z.string(),