diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/base/src/hostnameUtils.jsm | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/hostnameUtils.jsm b/comm/mailnews/base/src/hostnameUtils.jsm new file mode 100644 index 0000000000..e80c210b2e --- /dev/null +++ b/comm/mailnews/base/src/hostnameUtils.jsm @@ -0,0 +1,366 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Generic shared utility code for checking of IP and hostname validity. + */ + +const EXPORTED_SYMBOLS = [ + "isLegalHostNameOrIP", + "isLegalHostName", + "isLegalIPv4Address", + "isLegalIPv6Address", + "isLegalIPAddress", + "isLegalLocalIPAddress", + "cleanUpHostName", + "kMinPort", + "kMaxPort", +]; + +var kMinPort = 1; +var kMaxPort = 65535; + +/** + * Check if aHostName is an IP address or a valid hostname. + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in addition to decimal. + * @returns {?string} Unobscured host name if aHostName is valid. + * Returns null if it's not. + */ +function isLegalHostNameOrIP(aHostName, aAllowExtendedIPFormats) { + /* + RFC 1123: + Whenever a user inputs the identity of an Internet host, it SHOULD + be possible to enter either (1) a host domain name or (2) an IP + address in dotted-decimal ("#.#.#.#") form. The host SHOULD check + the string syntactically for a dotted-decimal number before + looking it up in the Domain Name System. + */ + + return ( + isLegalIPAddress(aHostName, aAllowExtendedIPFormats) || + isLegalHostName(aHostName) + ); +} + +/** + * Check if aHostName is a valid hostname. + * + * @returns {?string} The host name if it is valid. Returns null if it's not. + */ +function isLegalHostName(aHostName) { + /* + RFC 952: + A "name" (Net, Host, Gateway, or Domain name) is a text string up + to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus + sign (-), and period (.). Note that periods are only allowed when + they serve to delimit components of "domain style names". (See + RFC-921, "Domain Name System Implementation Schedule", for + background). No blank or space characters are permitted as part of a + name. No distinction is made between upper and lower case. The first + character must be an alpha character. The last character must not be + a minus sign or period. + + RFC 1123: + The syntax of a legal Internet host name was specified in RFC-952 + [DNS:4]. One aspect of host name syntax is hereby changed: the + restriction on the first character is relaxed to allow either a + letter or a digit. Host software MUST support this more liberal + syntax. + + Host software MUST handle host names of up to 63 characters and + SHOULD handle host names of up to 255 characters. + + RFC 1034: + Relative names are either taken relative to a well known origin, or to a + list of domains used as a search list. Relative names appear mostly at + the user interface, where their interpretation varies from + implementation to implementation, and in master files, where they are + relative to a single origin domain name. The most common interpretation + uses the root "." as either the single origin or as one of the members + of the search list, so a multi-label relative name is often one where + the trailing dot has been omitted to save typing. + + Since a complete domain name ends with the root label, this leads to + a printed form which ends in a dot. + */ + + const hostPattern = + /^(([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.?$/i; + return aHostName.length <= 255 && hostPattern.test(aHostName) + ? aHostName + : null; +} + +/** + * Check if aHostName is a valid IPv4 address. + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - If false, only IPv4 addresses + * in the common decimal format (4 components, each up to 255) + * will be accepted, no hex/octal formats. + * @returns {string} Unobscured canonicalized address if aHostName is an + * IPv4 address. Returns null if it's not. + */ +function isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) { + // Scammers frequently obscure the IP address by encoding each component as + // decimal, octal, hex or in some cases a mix match of each. There can even + // be less than 4 components where the last number covers the missing components. + // See the test at mailnews/base/test/unit/test_hostnameUtils.js for possible + // combinations. + + if (!aHostName) { + return null; + } + + // Break the IP address down into individual components. + let ipComponents = aHostName.split("."); + let componentCount = ipComponents.length; + if (componentCount > 4 || (componentCount < 4 && !aAllowExtendedIPFormats)) { + return null; + } + + /** + * Checks validity of an IP address component. + * + * @param {string} aValue - The component string. + * @param {integer} aWidth - How many components does this string cover. + * @returns {integer|null} The value of the component in decimal if it is valid. + * Returns null if it's not. + */ + const kPowersOf256 = [1, 256, 65536, 16777216, 4294967296]; + function isLegalIPv4Component(aValue, aWidth) { + let component; + // Is the component decimal? + if (/^(0|([1-9][0-9]{0,9}))$/.test(aValue)) { + component = parseInt(aValue, 10); + } else if (aAllowExtendedIPFormats) { + // Is the component octal? + if (/^(0[0-7]{1,12})$/.test(aValue)) { + component = parseInt(aValue, 8); + } else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) { + // The component is hex. + component = parseInt(aValue, 16); + } else { + return null; + } + } else { + return null; + } + + // Make sure the component in not larger than the expected maximum. + if (component >= kPowersOf256[aWidth]) { + return null; + } + + return component; + } + + for (let i = 0; i < componentCount; i++) { + // If we are on the last supplied component but we do not have 4, + // the last one covers the remaining ones. + let componentWidth = i == componentCount - 1 ? 4 - i : 1; + let componentValue = isLegalIPv4Component(ipComponents[i], componentWidth); + if (componentValue == null) { + return null; + } + + // If we have a component spanning multiple ones, split it. + for (let j = 0; j < componentWidth; j++) { + ipComponents[i + j] = + (componentValue >> ((componentWidth - 1 - j) * 8)) & 255; + } + } + + // First component of zero is not valid. + if (ipComponents[0] == 0) { + return null; + } + + return ipComponents.join("."); +} + +/** + * Check if aHostName is a valid IPv6 address. + * + * @param {string} aHostName - The string to check for validity. + * @returns {string} Unobscured canonicalized address if aHostName is an + * IPv6 address. Returns null if it's not. + */ +function isLegalIPv6Address(aHostName) { + if (!aHostName) { + return null; + } + + // Break the IP address down into individual components. + let ipComponents = aHostName.toLowerCase().split(":"); + + // Make sure there are at least 3 components. + if (ipComponents.length < 3) { + return null; + } + + let ipLength = ipComponents.length - 1; + + // Take care if the last part is written in decimal using dots as separators. + let lastPart = isLegalIPv4Address(ipComponents[ipLength], false); + if (lastPart) { + let lastPartComponents = lastPart.split("."); + // Convert it into standard IPv6 components. + ipComponents[ipLength] = ( + (lastPartComponents[0] << 8) | + lastPartComponents[1] + ).toString(16); + ipComponents[ipLength + 1] = ( + (lastPartComponents[2] << 8) | + lastPartComponents[3] + ).toString(16); + } + + // Make sure that there is only one empty component. + let emptyIndex; + for (let i = 1; i < ipComponents.length - 1; i++) { + if (ipComponents[i] == "") { + // If we already found an empty component return null. + if (emptyIndex) { + return null; + } + + emptyIndex = i; + } + } + + // If we found an empty component, extend it. + if (emptyIndex) { + ipComponents[emptyIndex] = 0; + + // Add components so we have a total of 8. + for (let count = ipComponents.length; count < 8; count++) { + ipComponents.splice(emptyIndex, 0, 0); + } + } + + // Make sure there are 8 components. + if (ipComponents.length != 8) { + return null; + } + + // Format all components to 4 character hex value. + for (let i = 0; i < ipComponents.length; i++) { + if (ipComponents[i] == "") { + ipComponents[i] = 0; + } + + // Make sure the component is a number and it isn't larger than 0xffff. + if (/^[0-9a-f]{1,4}$/.test(ipComponents[i])) { + ipComponents[i] = parseInt(ipComponents[i], 16); + if (isNaN(ipComponents[i]) || ipComponents[i] > 0xffff) { + return null; + } + } else { + return null; + } + + // Pad the component with 0:s. + ipComponents[i] = ("0000" + ipComponents[i].toString(16)).substr(-4); + } + + // TODO: support Zone indices in Link-local addresses? Currently they are rejected. + // http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices + + let hostName = ipComponents.join(":"); + // Treat 0000:0000:0000:0000:0000:0000:0000:0000 as an invalid IPv6 address. + return hostName != "0000:0000:0000:0000:0000:0000:0000:0000" + ? hostName + : null; +} + +/** + * Check if aHostName is a valid IP address (IPv4 or IPv6). + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in + * addition to decimal. + * @returns {?string} Unobscured canonicalized IPv4 or IPv6 address if it is + * valid, otherwise null. + */ +function isLegalIPAddress(aHostName, aAllowExtendedIPFormats) { + return ( + isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) || + isLegalIPv6Address(aHostName) + ); +} + +/** + * Check if aIPAddress is a local or private IP address. + * Note: if the passed in address is not in canonical (unobscured form), + * the result may be wrong. + * + * @param {string} aIPAddress - A valid IP address literal in canonical + * (unobscured) form. + * @returns {boolean} frue if it is a local/private IPv4 or IPv6 address. + */ +function isLegalLocalIPAddress(aIPAddress) { + // IPv4 address? + let ipComponents = aIPAddress.split("."); + if (ipComponents.length == 4) { + // Check if it's a local or private IPv4 address. + return ( + ipComponents[0] == 10 || + ipComponents[0] == 127 || // loopback address + (ipComponents[0] == 192 && ipComponents[1] == 168) || + (ipComponents[0] == 169 && ipComponents[1] == 254) || + (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32) + ); + } + + // IPv6 address? + ipComponents = aIPAddress.split(":"); + if (ipComponents.length == 8) { + // ::1/128 - localhost + if ( + ipComponents[0] == "0000" && + ipComponents[1] == "0000" && + ipComponents[2] == "0000" && + ipComponents[3] == "0000" && + ipComponents[4] == "0000" && + ipComponents[5] == "0000" && + ipComponents[6] == "0000" && + ipComponents[7] == "0001" + ) { + return true; + } + + // fe80::/10 - link local addresses + if (ipComponents[0] == "fe80") { + return true; + } + + // fc00::/7 - unique local addresses + if ( + ipComponents[0].startsWith("fc") || // usage has not been defined yet + ipComponents[0].startsWith("fd") + ) { + return true; + } + + return false; + } + + return false; +} + +/** + * Clean up the hostname or IP. Usually used to sanitize a value input by the user. + * It is usually applied before we know if the hostname is even valid. + * + * @param {string} aHostName - The hostname or IP string to clean up. + */ +function cleanUpHostName(aHostName) { + // TODO: Bug 235312: if UTF8 string was input, convert to punycode using convertUTF8toACE() + // but bug 563172 needs resolving first. + return aHostName.trim(); +} |