Files
proxy/nodejs/public/lib/js/val.js
T
wmantly a9dfebc481 Allow single-label hostnames as a Host target (#126)
The target ("ip") field validator required at least two dot-separated
labels, rejecting legitimate single-label hostnames like Docker Compose
service names ("sso-manager"), /etc/hosts entries, or anything resolved
via a search domain. This was enforced identically client-side
(public/lib/js/val.js) and server-side (utils/hostname_validate.js,
routes/host.js), so there was no way to set one through the UI or API
— only by writing to the Host model directly, bypassing validation
entirely (which is how theta-env's setup.sh registers sso-manager as a
target today, working only because it calls Host.create() directly).

Relax HOSTNAME in both places to accept either a bare single label or
the existing dotted-FQDN pattern. Flips the one existing test that
codified the old behavior (isValidHostname('localhost') was asserted
false) and adds coverage for the reported case.
2026-07-14 01:12:02 -04:00

201 lines
5.4 KiB
JavaScript
Executable File

( function( $ ) {
var settings = {
rule: {
eq: function(value, options){
var compare = $('[name=' + options + ']').val();
if ( value != compare ) {
return "Miss-match";
}
}
},
};
$.fn.validate = function(event) {
// let thisSettings = $.extend(true, settings, settingsObj);
let hasErrors = false;
if(this.is('[validate]')) return this.validateField(event);
if(!this.attr('isValid')){
console.log('adding reset event')
this.on('reset', function(){
$(this).attr('isValid', false);
$(this).validateClear();
})
}
this.find('[validate]').each(function(){
if(!$(this).validateField()) hasErrors = true;
});
this.attr('isValid', !hasErrors);
if(hasErrors && event) event.preventDefault();
return !hasErrors;
};
$.fn.validateClear = function(){
$(this).find('input').each(function(){
$(this).removeClass('is-invalid');
$(this).removeClass('is-valid');
})
}
$.fn.validateField = function(){
var attr = this.attr('validate').split(':'); //array of params
var rule = attr[0];
var options = attr[1];
var value = this.val(); //link to input value
var message;
if(this.prop('disabled')) return true;
//checks if field is required, and length
if(!isNaN(options) && value.length < options){
message = `Must be ${options} characters`;
}
//checks if empty to stop processing
if(!isNaN(options) && value.length === 0) {
}else if(rule in settings.rule){
message = settings.rule[rule].apply(this, [value, options]);
}
this.validateMessage(message)
return !message;
}
$.fn.validateMessage = function(message){
if(message && message !== true){
this.closest('.form-group').find('b.invalid-feedback').html(message);
this.addClass('is-invalid');
}else{
this.removeClass('is-invalid');
this.addClass('is-valid');
}
return this;
};
jQuery.extend({
validateSettings: function( settingsObj ) {
$.extend( true, settings, settingsObj );
},
validateInit: function( ettingsObj ) {
$( '[action]' ).on( 'submit', function ( event, settingsObj ){
$( this ).validate( settingsObj, event );
});
}
});
}( jQuery ));
// Host / target validation, mirrored from the backend (utils/hostname_validate.js):
// a bare hostname or IPv4 address, no protocol / "/" / ":" / whitespace. The
// incoming host may be a wildcard ("*.example.com"); the target may not.
(function(){
var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
// Either one bare label (Docker service names, /etc/hosts entries) or a
// dotted hostname with an alphabetic TLD.
var HOSTNAME = /^(?=.{1,253}$)(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)$/i;
var FORBIDDEN = /[\s/:]/;
function isIPv4( value ) {
var parts = value.split( '.' );
if ( parts.length !== 4 ) return false;
return parts.every( function( p ) {
return /^(0|[1-9]\d{0,2})$/.test( p ) && Number( p ) <= 255;
});
}
// Incoming-host pattern: labels may be normal, "*" (one fragment), or "**"
// (any number of fragments, incl. a bare "**" global catch-all).
function isHostPattern( value ) {
if ( value.length > 253 ) return false;
return value.split( '.' ).every( function( l ) {
return l === '*' || l === '**' || LABEL.test( l );
});
}
function forbidden( value ) {
return FORBIDDEN.test( value ) || value.includes( '://' );
}
// Incoming host: IPv4 or a wildcard host pattern.
function checkHost( value ) {
if ( typeof value !== 'string' || value.length === 0 ) return "Required";
if ( forbidden( value ) ) return 'No protocol, "/", or ":"';
if ( isIPv4( value ) || isHostPattern( value ) ) return;
return "Enter a valid host or wildcard (*, **)";
}
// Downstream target: IPv4 or a strict hostname, no wildcard.
function checkTarget( value ) {
if ( typeof value !== 'string' || value.length === 0 ) return "Required";
if ( forbidden( value ) ) return 'No protocol, "/", or ":"';
if ( isIPv4( value ) || HOSTNAME.test( value ) ) return;
return "Enter a valid hostname or IP";
}
$.validateSettings({
rule:{
ip: function( value ) {
value = value.split( '.' );
if ( value.length != 4 ) {
return "Malformed IP";
}
$.each( value, function( key, value ) {
if( value > 255 || value < 0 ) {
return "Malformed IP";
}
});
},
// Incoming host name — hostname, IPv4, or wildcard pattern (*, **).
host: function( value ) {
return checkHost( value );
},
// Downstream target — hostname or IPv4, no wildcard.
target: function( value ) {
return checkTarget( value );
},
// Back-compat alias (no wildcard).
hostname: function( value ) {
return checkTarget( value );
},
user: function( value ) {
var reg = /^[a-z0-9\_\-\@\.]{1,32}$/;
if ( reg.test( value ) === false ) {
return "Invalid";
}
},
// Mirrors utils/password_policy.js: >= 8 chars, and either 12+ chars
// or at least 3 of {lowercase, uppercase, number, symbol}.
password: function( value ) {
if ( typeof value !== 'string' || value.length < 8 ) {
return "Password must be at least 8 characters";
}
if ( value.length >= 12 ) return;
var classes = 0;
if ( /[a-z]/.test( value ) ) classes++;
if ( /[A-Z]/.test( value ) ) classes++;
if ( /[0-9]/.test( value ) ) classes++;
if ( /[^A-Za-z0-9]/.test( value ) ) classes++;
if ( classes < 3 ) {
return "Use 3 of: lowercase, uppercase, number, symbol (or 12+ chars)";
}
}
}
});
})();