mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Merge branch 'dev' into clients-user
This commit is contained in:
@@ -7,20 +7,20 @@ export const SiteSchema = z.object({
|
||||
|
||||
export const TargetHealthCheckSchema = z.object({
|
||||
hostname: z.string(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
port: z.int().min(1).max(65535),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
path: z.string().optional(),
|
||||
scheme: z.string().optional(),
|
||||
mode: z.string().default("http"),
|
||||
interval: z.number().int().default(30),
|
||||
"unhealthy-interval": z.number().int().default(30),
|
||||
unhealthyInterval: z.number().int().optional(), // deprecated alias
|
||||
timeout: z.number().int().default(5),
|
||||
interval: z.int().default(30),
|
||||
"unhealthy-interval": z.int().default(30),
|
||||
unhealthyInterval: z.int().optional(), // deprecated alias
|
||||
timeout: z.int().default(5),
|
||||
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
|
||||
"follow-redirects": z.boolean().default(true),
|
||||
followRedirects: z.boolean().optional(), // deprecated alias
|
||||
method: z.string().default("GET"),
|
||||
status: z.number().int().optional()
|
||||
status: z.int().optional()
|
||||
});
|
||||
|
||||
// Schema for individual target within a resource
|
||||
@@ -28,16 +28,16 @@ export const TargetSchema = z.object({
|
||||
site: z.string().optional(),
|
||||
method: z.enum(["http", "https", "h2c"]).optional(),
|
||||
hostname: z.string(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
port: z.int().min(1).max(65535),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
"internal-port": z.number().int().min(1).max(65535).optional(),
|
||||
"internal-port": z.int().min(1).max(65535).optional(),
|
||||
path: z.string().optional(),
|
||||
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||
healthcheck: TargetHealthCheckSchema.optional(),
|
||||
rewritePath: z.string().optional(), // deprecated alias
|
||||
"rewrite-path": z.string().optional(),
|
||||
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
||||
priority: z.number().int().min(1).max(1000).optional().default(100)
|
||||
priority: z.int().min(1).max(1000).optional().default(100)
|
||||
});
|
||||
export type TargetData = z.infer<typeof TargetSchema>;
|
||||
|
||||
@@ -55,10 +55,10 @@ export const AuthSchema = z.object({
|
||||
.optional()
|
||||
.default([])
|
||||
.refine((roles) => !roles.includes("Admin"), {
|
||||
message: "Admin role cannot be included in sso-roles"
|
||||
error: "Admin role cannot be included in sso-roles"
|
||||
}),
|
||||
"sso-users": z.array(z.string().email()).optional().default([]),
|
||||
"whitelist-users": z.array(z.string().email()).optional().default([]),
|
||||
"sso-users": z.array(z.email()).optional().default([]),
|
||||
"whitelist-users": z.array(z.email()).optional().default([]),
|
||||
});
|
||||
|
||||
export const RuleSchema = z.object({
|
||||
@@ -79,7 +79,7 @@ export const ResourceSchema = z
|
||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
"full-domain": z.string().optional(),
|
||||
"proxy-port": z.number().int().min(1).max(65535).optional(),
|
||||
"proxy-port": z.int().min(1).max(65535).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
targets: z.array(TargetSchema.nullable()).optional().default([]),
|
||||
auth: AuthSchema.optional(),
|
||||
@@ -100,9 +100,8 @@ export const ResourceSchema = z
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum",
|
||||
path: ["name", "protocol"]
|
||||
path: ["name", "protocol"],
|
||||
error: "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -117,6 +116,20 @@ export const ResourceSchema = z
|
||||
(target) => target == null || target.method !== undefined
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["targets"],
|
||||
error: "When protocol is 'http', all targets must have a 'method' field"
|
||||
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(resource) => {
|
||||
if (isTargetsOnlyResource(resource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If protocol is tcp or udp, no target should have method field
|
||||
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
||||
return resource.targets.every(
|
||||
@@ -125,19 +138,9 @@ export const ResourceSchema = z
|
||||
}
|
||||
return true;
|
||||
},
|
||||
(resource) => {
|
||||
if (resource.protocol === "http") {
|
||||
return {
|
||||
message:
|
||||
"When protocol is 'http', all targets must have a 'method' field",
|
||||
path: ["targets"]
|
||||
};
|
||||
}
|
||||
return {
|
||||
message:
|
||||
"When protocol is 'tcp' or 'udp', targets must not have a 'method' field",
|
||||
path: ["targets"]
|
||||
};
|
||||
{
|
||||
path: ["targets"],
|
||||
error: "When protocol is 'tcp' or 'udp', targets must not have a 'method' field"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -156,9 +159,8 @@ export const ResourceSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"When protocol is 'http', a 'full-domain' must be provided",
|
||||
path: ["full-domain"]
|
||||
path: ["full-domain"],
|
||||
error: "When protocol is 'http', a 'full-domain' must be provided"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -174,9 +176,8 @@ export const ResourceSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"When protocol is 'tcp' or 'udp', 'proxy-port' must be provided",
|
||||
path: ["proxy-port", "exit-node"]
|
||||
path: ["proxy-port", "exit-node"],
|
||||
error: "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -193,9 +194,8 @@ export const ResourceSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"When protocol is 'tcp' or 'udp', 'auth' must not be provided",
|
||||
path: ["auth"]
|
||||
path: ["auth"],
|
||||
error: "When protocol is 'tcp' or 'udp', 'auth' must not be provided"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -216,36 +216,12 @@ export const ClientResourceSchema = z.object({
|
||||
// Schema for the entire configuration object
|
||||
export const ConfigSchema = z
|
||||
.object({
|
||||
"proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}),
|
||||
"client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}),
|
||||
sites: z.record(z.string(), SiteSchema).optional().default({})
|
||||
"proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}),
|
||||
"client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}),
|
||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||
})
|
||||
.refine(
|
||||
// Enforce the full-domain uniqueness across resources in the same stack
|
||||
(config) => {
|
||||
// Extract all full-domain values with their resource keys
|
||||
const fullDomainMap = new Map<string, string[]>();
|
||||
|
||||
Object.entries(config["proxy-resources"]).forEach(
|
||||
([resourceKey, resource]) => {
|
||||
const fullDomain = resource["full-domain"];
|
||||
if (fullDomain) {
|
||||
// Only process if full-domain is defined
|
||||
if (!fullDomainMap.has(fullDomain)) {
|
||||
fullDomainMap.set(fullDomain, []);
|
||||
}
|
||||
fullDomainMap.get(fullDomain)!.push(resourceKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Find duplicates
|
||||
const duplicates = Array.from(fullDomainMap.entries()).filter(
|
||||
([_, resourceKeys]) => resourceKeys.length > 1
|
||||
);
|
||||
|
||||
return duplicates.length === 0;
|
||||
},
|
||||
(config) => {
|
||||
// Extract duplicates for error message
|
||||
const fullDomainMap = new Map<string, string[]>();
|
||||
@@ -271,38 +247,16 @@ export const ConfigSchema = z
|
||||
)
|
||||
.join("; ");
|
||||
|
||||
return {
|
||||
message: `Duplicate 'full-domain' values found: ${duplicates}`,
|
||||
path: ["resources"]
|
||||
};
|
||||
if (duplicates.length !== 0) {
|
||||
return {
|
||||
path: ["resources"],
|
||||
error: `Duplicate 'full-domain' values found: ${duplicates}`
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
// Enforce proxy-port uniqueness within proxy-resources per protocol
|
||||
(config) => {
|
||||
const protocolPortMap = new Map<string, string[]>();
|
||||
|
||||
Object.entries(config["proxy-resources"]).forEach(
|
||||
([resourceKey, resource]) => {
|
||||
const proxyPort = resource["proxy-port"];
|
||||
const protocol = resource.protocol;
|
||||
if (proxyPort !== undefined && protocol !== undefined) {
|
||||
const key = `${protocol}:${proxyPort}`;
|
||||
if (!protocolPortMap.has(key)) {
|
||||
protocolPortMap.set(key, []);
|
||||
}
|
||||
protocolPortMap.get(key)!.push(resourceKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Find duplicates
|
||||
const duplicates = Array.from(protocolPortMap.entries()).filter(
|
||||
([_, resourceKeys]) => resourceKeys.length > 1
|
||||
);
|
||||
|
||||
return duplicates.length === 0;
|
||||
},
|
||||
(config) => {
|
||||
// Extract duplicates for error message
|
||||
const protocolPortMap = new Map<string, string[]>();
|
||||
@@ -331,36 +285,16 @@ export const ConfigSchema = z
|
||||
)
|
||||
.join("; ");
|
||||
|
||||
return {
|
||||
message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`,
|
||||
path: ["proxy-resources"]
|
||||
};
|
||||
if (duplicates.length !== 0) {
|
||||
return {
|
||||
path: ["proxy-resources"],
|
||||
error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
// Enforce proxy-port uniqueness within client-resources
|
||||
(config) => {
|
||||
const proxyPortMap = new Map<number, string[]>();
|
||||
|
||||
Object.entries(config["client-resources"]).forEach(
|
||||
([resourceKey, resource]) => {
|
||||
const proxyPort = resource["proxy-port"];
|
||||
if (proxyPort !== undefined) {
|
||||
if (!proxyPortMap.has(proxyPort)) {
|
||||
proxyPortMap.set(proxyPort, []);
|
||||
}
|
||||
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Find duplicates
|
||||
const duplicates = Array.from(proxyPortMap.entries()).filter(
|
||||
([_, resourceKeys]) => resourceKeys.length > 1
|
||||
);
|
||||
|
||||
return duplicates.length === 0;
|
||||
},
|
||||
(config) => {
|
||||
// Extract duplicates for error message
|
||||
const proxyPortMap = new Map<number, string[]>();
|
||||
@@ -385,10 +319,12 @@ export const ConfigSchema = z
|
||||
)
|
||||
.join("; ");
|
||||
|
||||
return {
|
||||
message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`,
|
||||
path: ["client-resources"]
|
||||
};
|
||||
if (duplicates.length !== 0) {
|
||||
return {
|
||||
path: ["client-resources"],
|
||||
error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -89,6 +89,16 @@ export class Config {
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||
.notifications.product_updates
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||
.notifications.new_releases
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
if (parsedConfig.server.maxmind_db_path) {
|
||||
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
|
||||
}
|
||||
@@ -158,7 +168,7 @@ export class Config {
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.fossorial.io/api/v1/license/validate",
|
||||
`https://api.fossorial.io/api/v1/license/validate`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -308,7 +308,7 @@ export function generateRemoteSubnetsStr(allSiteResources: SiteResource[]) {
|
||||
if (sr.mode === "cidr") return true;
|
||||
if (sr.mode === "host") {
|
||||
// check if its a valid IP using zod
|
||||
const ipSchema = z.string().ip();
|
||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||
const parseResult = ipSchema.safeParse(sr.destination);
|
||||
return parseResult.success;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,8 @@ export const configSchema = z
|
||||
.object({
|
||||
app: z
|
||||
.object({
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.pipe(z.string().url())
|
||||
dashboard_url: z.url()
|
||||
.pipe(z.url())
|
||||
.transform((url) => url.toLowerCase())
|
||||
.optional(),
|
||||
log_level: z
|
||||
@@ -31,7 +29,14 @@ export const configSchema = z
|
||||
anonymous_usage: z.boolean().optional().default(true)
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
.prefault({}),
|
||||
notifications: z
|
||||
.object({
|
||||
product_updates: z.boolean().optional().default(true),
|
||||
new_releases: z.boolean().optional().default(true)
|
||||
})
|
||||
.optional()
|
||||
.prefault({})
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
@@ -40,6 +45,10 @@ export const configSchema = z
|
||||
log_failed_attempts: false,
|
||||
telemetry: {
|
||||
anonymous_usage: true
|
||||
},
|
||||
notifications: {
|
||||
product_updates: true,
|
||||
new_releases: true
|
||||
}
|
||||
}),
|
||||
domains: z
|
||||
@@ -96,7 +105,7 @@ export const configSchema = z
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
.prefault({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -121,7 +130,7 @@ export const configSchema = z
|
||||
credentials: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||
trust_proxy: z.int().gte(0).optional().default(1),
|
||||
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||
maxmind_db_path: z.string().optional()
|
||||
})
|
||||
@@ -178,7 +187,7 @@ export const configSchema = z
|
||||
.default(5000)
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
.prefault({})
|
||||
})
|
||||
.optional(),
|
||||
traefik: z
|
||||
@@ -205,10 +214,13 @@ export const configSchema = z
|
||||
.default(["newt", "wireguard", "local"]),
|
||||
allow_raw_resources: z.boolean().optional().default(true),
|
||||
file_mode: z.boolean().optional().default(false),
|
||||
pp_transport_prefix: z.string().optional().default("pp-transport-v")
|
||||
pp_transport_prefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pp-transport-v")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
.prefault({}),
|
||||
gerbil: z
|
||||
.object({
|
||||
exit_node_name: z.string().optional(),
|
||||
@@ -233,7 +245,7 @@ export const configSchema = z
|
||||
.default(30)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
.prefault({}),
|
||||
orgs: z
|
||||
.object({
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
@@ -262,7 +274,7 @@ export const configSchema = z
|
||||
.default(500)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
.prefault({}),
|
||||
auth: z
|
||||
.object({
|
||||
window_minutes: z
|
||||
@@ -279,10 +291,10 @@ export const configSchema = z
|
||||
.default(500)
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
.prefault({})
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
.prefault({}),
|
||||
email: z
|
||||
.object({
|
||||
smtp_host: z.string().optional(),
|
||||
@@ -294,7 +306,7 @@ export const configSchema = z
|
||||
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
|
||||
smtp_secure: z.boolean().optional(),
|
||||
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
||||
no_reply: z.string().email().optional()
|
||||
no_reply: z.email().optional()
|
||||
})
|
||||
.optional(),
|
||||
flags: z
|
||||
@@ -315,11 +327,18 @@ export const configSchema = z
|
||||
nameservers: z
|
||||
.array(z.string().optional().optional())
|
||||
.optional()
|
||||
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]),
|
||||
cname_extension: z.string().optional().default("cname.pangolin.net")
|
||||
.default([
|
||||
"ns1.pangolin.net",
|
||||
"ns2.pangolin.net",
|
||||
"ns3.pangolin.net"
|
||||
]),
|
||||
cname_extension: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("cname.pangolin.net")
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
.prefault({})
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -334,7 +353,7 @@ export const configSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "At least one domain must be defined"
|
||||
error: "At least one domain must be defined"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -349,7 +368,7 @@ export const configSchema = z
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Server secret must be defined"
|
||||
error: "Server secret must be defined"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -361,7 +380,7 @@ export const configSchema = z
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Dashboard URL must be defined"
|
||||
error: "Dashboard URL must be defined"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class TelemetryClient {
|
||||
license_tier: licenseStatus.tier || "unknown"
|
||||
}
|
||||
};
|
||||
logger.debug("Sending enterprise startup telemtry payload:", {
|
||||
logger.debug("Sending enterprise startup telemetry payload:", {
|
||||
payload
|
||||
});
|
||||
// this.client.capture(payload);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import z from "zod";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
export function isValidCIDR(cidr: string): boolean {
|
||||
return z.string().cidr().safeParse(cidr).success;
|
||||
return z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success;
|
||||
}
|
||||
|
||||
export function isValidIP(ip: string): boolean {
|
||||
return z.string().ip().safeParse(ip).success;
|
||||
return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
|
||||
}
|
||||
|
||||
export function isValidUrlGlobPattern(pattern: string): boolean {
|
||||
@@ -68,11 +69,11 @@ 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_]*)?$",
|
||||
"((([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);
|
||||
@@ -83,12 +84,15 @@ export function isTargetValid(value: string | undefined) {
|
||||
|
||||
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;
|
||||
// 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;
|
||||
try {
|
||||
const addr = ipaddr.parse(value);
|
||||
return addr.kind() === "ipv4" || addr.kind() === "ipv6";
|
||||
} catch {
|
||||
// fall through to domain regex check
|
||||
}
|
||||
|
||||
return DOMAIN_REGEX.test(value);
|
||||
@@ -169,10 +173,10 @@ export function isSecondLevelDomain(domain: string): boolean {
|
||||
}
|
||||
|
||||
const trimmedDomain = domain.trim().toLowerCase();
|
||||
|
||||
|
||||
// Split into parts
|
||||
const parts = trimmedDomain.split('.');
|
||||
|
||||
|
||||
// Should have exactly 2 parts for a second-level domain (e.g., "example.com")
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user