Files
proxy/nodejs/utils/hostname_validate.js
T
wmantly 6d8dc45209 Validate host/target fields (hostname or IP; host allows */** wildcards)
Backend (utils/hostname_validate.js, enforced in routes/host.js on create/update):
- host: IPv4 or a wildcard pattern whose labels may be normal, "*" (one
  fragment) or "**" (any depth, incl. a bare "**" catch-all) — matching
  Host.lookUp. Lowered Host.host min length to 1 so "**"/"*" pass the model.
- target (ip): IPv4 or a strict hostname, no wildcards.
- Both reject scheme, "/", ":" and whitespace; 422 with per-field keys.

Frontend (val.js) mirrors the rules: host/target validators + hosts.ejs fields
point at them. Unit tests in test/unit/hostname_validate.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:18:47 -04:00

97 lines
3.7 KiB
JavaScript

'use strict';
/**
* Validation for the user-supplied host / target fields on a Host entry.
*
* Neither field may carry a scheme (http://), a path ("/"), a port or ":" of any
* kind, or whitespace.
*
* host (incoming) — an IPv4 address or a hostname pattern whose dot-separated
* labels may be normal DNS labels or wildcard fragments:
* "*" matches exactly one subdomain fragment
* "**" matches any number of fragments
* e.g. "*.example.com", "**.mysite.com", "payments.**", and
* a bare "**" as a global catch-all. (Matched by
* Host.lookUp in models/host.js.)
* ip (target) — a concrete destination: an IPv4 address or a strict
* hostname (dotted, alphabetic TLD). No wildcards.
*
* Pure (no I/O) so it can be unit tested and reused. Enforced at the route layer
* (routes/host.js) so internally-created entries (wildcard children, on-demand
* cache) are unaffected.
*/
// A single DNS label.
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
// A strict hostname: dotted labels + alphabetic TLD (for the target).
const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
// Scheme, path, port, or whitespace — anything that means it isn't a bare host.
const FORBIDDEN = /[\s/:]/;
function isValidIPv4(value){
if(typeof value !== 'string') return false;
let parts = value.split('.');
if(parts.length !== 4) return false;
// Each octet 0-255, no leading zeros (0 itself is fine).
return parts.every(p => /^(0|[1-9]\d{0,2})$/.test(p) && Number(p) <= 255);
}
// A strict, concrete hostname (used for the downstream target). No wildcards.
function isValidHostname(value){
return typeof value === 'string' && HOSTNAME.test(value);
}
// An incoming-host pattern: dot-separated labels, each a normal label or a
// wildcard fragment ("*" / "**"). A bare "**" is the global catch-all.
function isValidHostPattern(value){
if(typeof value !== 'string' || value.length === 0 || value.length > 253) return false;
return value.split('.').every(l => l === '*' || l === '**' || LABEL.test(l));
}
// The incoming `host` field: IPv4 or a wildcard host pattern, no forbidden chars.
function isValidHostField(value){
if(typeof value !== 'string' || value.length === 0) return false;
if(FORBIDDEN.test(value)) return false;
return isValidIPv4(value) || isValidHostPattern(value);
}
// The `ip` (target) field: IPv4 or a strict hostname, no forbidden chars.
function isValidTargetField(value){
if(typeof value !== 'string' || value.length === 0) return false;
if(FORBIDDEN.test(value)) return false;
return isValidIPv4(value) || isValidHostname(value);
}
const NO_CHARS = 'no protocol, "/", or ":".';
/**
* Collect {key, message} errors for whichever of host / ip are present on the
* body. Absent fields are skipped (presence/length is handled by the model), so
* this works for both create (both present) and partial update.
*/
function collectHostFieldErrors(body){
let errors = [];
body = body || {};
if(body.host !== undefined && body.host !== null && body.host !== ''){
if(!isValidHostField(body.host)){
errors.push({key: 'host', message: `Host must be a hostname, IP, or wildcard pattern (*, **) — ${NO_CHARS}`});
}
}
if(body.ip !== undefined && body.ip !== null && body.ip !== ''){
if(!isValidTargetField(body.ip)){
errors.push({key: 'ip', message: `Target must be a valid hostname or IP address — ${NO_CHARS}`});
}
}
return errors;
}
module.exports = {
isValidIPv4,
isValidHostname,
isValidHostPattern,
isValidHostField,
isValidTargetField,
collectHostFieldErrors,
};