diff options
Diffstat (limited to 'mailnews/base/util/hostnameUtils.jsm')
-rw-r--r-- | mailnews/base/util/hostnameUtils.jsm | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/mailnews/base/util/hostnameUtils.jsm b/mailnews/base/util/hostnameUtils.jsm new file mode 100644 index 000000000..e2d0ee4ff --- /dev/null +++ b/mailnews/base/util/hostnameUtils.jsm @@ -0,0 +1,341 @@ +/* -*- 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. + */ + +this.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 aHostName The string to check for validity. + * @param aAllowExtendedIPFormats Allow hex/octal formats in addition to decimal. + * @return 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. + * + * @return 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 aHostName The string to check for validity. + * @param aAllowExtendedIPFormats If false, only IPv4 addresses in the common + decimal format (4 components, each up to 255) + * will be accepted, no hex/octal formats. + * @return 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 aValue The component string. + * @param aWidth How many components does this string cover. + * @return 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); + // Is the component hex? + else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) + 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 aHostName The string to check for validity. + * @return 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 aHostName The string to check for validity. + * @param aAllowExtendedIPFormats Allow hex/octal formats in addition to decimal. + * @return 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. + * + * @param aIPAddress A valid IP address literal in canonical (unobscured) form. + * @return True if it is a local/private IPv4 or IPv6 address, + * otherwise false. + * + * Note: if the passed in address is not in canonical (unobscured form), + * the result may be wrong. + */ +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 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(); +} |