passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
204 lines (179 loc) • 6.68 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 5.2.0
*/
import XRegExp from "xregexp";
import ipRegex from "ip-regex";
import assertString from "validator/es/lib/util/assertString";
// Hostname allowed characters regex
const regexHostnameAllowedChars = XRegExp("^[\\p{L}\\p{N}.-]*$");
/**
* The can suggest service
*/
class CanSuggestService {
/**
* Check if an uri can be suggested for an array of uris.
* @param {string} uri
* @param {Array<string>} suggestedUris
* @returns {boolean}
*/
canSuggestUris(uri, suggestedUris) {
assertString(uri);
return suggestedUris?.some((suggestedUri) => this.canSuggestUri(uri, suggestedUri)) || false;
}
/**
* Check if an uri can be suggested for a given one.
* The uris can be suggested if:
* - The uris hostname match;
* - The suggested uri is a parent of the uri;
* - (optional check) If the suggested uri contains a protocol, then it has to match the uri.
* - (optional check) If the suggested uri contains a port, then it has to match the uri.
*
* Note that this function does not take in account any uris path, parameters or hash.
*
* @param {string} uri The uri.
* @param {string} suggestedUri The potential uri to suggest
* @returns {boolean}
*/
canSuggestUri(uri, suggestedUri) {
assertString(uri);
assertString(suggestedUri);
let uriObject;
try {
uriObject = new URL(uri);
} catch (error) {
/*
* Only valid uri are accepted by this function.
* This information should come from the browser tab uri, and so should be valid.
*/
console.error(`Invalid URI "${uri}":`, error);
return false;
}
const suggestedUriObject = this.parseSuggestedUri(suggestedUri);
// Unable to parse the hostname of the uri
if (!uriObject || !uriObject.hostname) {
return false;
}
// Unable to parse the suggested uri or the suggested uri has no hostname.
if (!suggestedUriObject || !suggestedUriObject.hostname) {
return false;
}
// Check the protocol, if the suggest uri has defined it.
if (suggestedUriObject.protocol) {
if (uriObject.protocol !== suggestedUriObject.protocol) {
return false;
}
}
// Check the port, if the suggest uri has defined it.
if (suggestedUriObject.port) {
if (uriObject.port !== suggestedUriObject.port) {
return false;
}
}
// Check the hostname, if the suggest uri has defined it. Perfect match
if (uriObject.hostname === suggestedUriObject.hostname) {
return true;
}
// If IPs, make a strict comparison.
const uriIsIpAddress = ipRegex({ exact: true }).test(uriObject.hostname);
const suggestUriIsIpAddress = ipRegex({ exact: true }).test(suggestedUriObject.hostname);
if (uriIsIpAddress || suggestUriIsIpAddress) {
return uriObject.hostname === suggestedUriObject.hostname;
}
// Otherwise check if the suggested uri hostname contain a dot and is a parent host of the uri hostname
if (suggestedUriObject.hostname.indexOf(".") !== -1) {
return this.isParentHostname(suggestedUriObject.hostname, uriObject.hostname);
}
return false;
}
/**
* Parse a suggested uri.
* @private
* @param {string} suggestedUri The uri.
* @returns {{protocol: string, hostname: string, port: string}}
*/
parseSuggestedUri(suggestedUri) {
let suggestedUriObject;
let protocol = "";
let hostname = "";
let port = "";
let enforceProtocol = false;
/*
* The browser URL primitive does not work with relative uris.
* Enforce a protocol to the suggested uri, if none has been defined. A fake protocol is used in order to preserve
* the port information that can be altered by the parsing. By instance, when the https is enforced to the uri
* www.passbolt.com:443, then the port information is deleted after the parsing.
*/
if (!/^[a-z\-]*:\/\//i.test(suggestedUri)) {
enforceProtocol = true;
suggestedUri = `fake://${suggestedUri}`;
}
try {
suggestedUriObject = new URL(suggestedUri);
} catch {
return false;
}
port = suggestedUriObject.port;
if (!enforceProtocol) {
protocol = suggestedUriObject.protocol;
hostname = suggestedUriObject.hostname;
} else {
suggestedUriObject.protocol = "https:";
hostname = suggestedUriObject.hostname;
}
return { protocol: protocol, hostname: hostname, port: port };
}
/**
* Check a hostname is parent of another on.
* Note, the function does not ensure the validity of the hostnames.
*
* By instance for a given hostname: accounts.passbolt.com
* The following hostnames should match:
* - accounts.passbolt.com
* - passbolt.com
*
* The following hostnames should not match:
* - passbolt.com.attacker.com
* - attacker-passbolt.com
*
* @private
* @param {string} parent the hostname to check if it is parent.
* @param {string} child The hostname to check if it is a child.
* @return {boolean}
*/
isParentHostname(parent, child) {
if (!child || !parent || !regexHostnameAllowedChars.test(child) || !regexHostnameAllowedChars.test(parent)) {
return false;
}
const lastIndexOf = child.lastIndexOf(parent);
// If found.
if (lastIndexOf !== -1) {
/*
* The resource hostname has to be the last part of the given hostname.
* It will prevent an attacker to use a hostname such as www.passbolt.com.attacker.com, and make passbolt
* recognize it as passbolt.com.
*/
if (lastIndexOf + parent.length === child.length) {
/*
* Whatever is found before the resource hostname in the hostname has to be a subdomain or nan.
* It will prevent an attacker to use a hostname such as www.attacher-passbolt.com, and make passbolt
* recognize it as passbolt.com.
*/
if (child[lastIndexOf - 1] === undefined || child[lastIndexOf - 1] === ".") {
return true;
}
}
}
return false;
}
}
export default new CanSuggestService();