32 lines
1.2 KiB
JavaScript
32 lines
1.2 KiB
JavaScript
'use strict';
|
|||
|
|
|
||
|
|
/**
|
||
|
|
* Validate a backend/UI URL without accepting credential-bearing or opaque URLs.
|
||
|
|
* Query strings and fragments are rejected so secrets cannot be persisted in the URL.
|
||
|
|
*/
|
||
|
|
function validateBackendUrl(value) {
|
||
|
|
if (typeof value !== 'string') return { valid: false, error: 'URL must be text.' };
|
||
|
|
const input = value.trim();
|
||
|
|
if (!input || input.length > 2048) return { valid: false, error: 'Enter a URL up to 2048 characters.' };
|
||
|
|
|
||
|
|
let parsed;
|
||
|
|
try {
|
||
|
|
parsed = new URL(input);
|
||
|
|
} catch {
|
||
|
|
return { valid: false, error: 'Enter a complete http:// or https:// URL.' };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||
|
|
return { valid: false, error: 'Only http:// and https:// URLs are supported.' };
|
||
|
|
}
|
||
|
|
if (!parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
||
|
|
return { valid: false, error: 'URL must not contain credentials, query parameters, or fragments.' };
|
||
|
|
}
|
||
|
|
if (/\s/.test(parsed.hostname) || parsed.hostname.includes('..')) {
|
||
|
|
return { valid: false, error: 'Enter a valid hostname.' };
|
||
|
|
}
|
||
|
|
return { valid: true, value: parsed.toString().replace(/\/$/, '') };
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { validateBackendUrl };
|