| /* |
| * Copyright (C) 2005-2019 Apple Inc. All rights reserved. |
| * Copyright (C) 2018 Igalia S.L. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "URLHelpers.h" |
| |
| #include "URLParser.h" |
| #include <mutex> |
| #include <unicode/uidna.h> |
| #include <unicode/uscript.h> |
| #include <wtf/text/WTFString.h> |
| |
| namespace WTF { |
| namespace URLHelpers { |
| |
| constexpr unsigned urlBytesBufferLength = 2048; |
| |
| // This needs to be higher than the UScriptCode for any of the scripts on the IDN allowed list. |
| // At one point we used USCRIPT_CODE_LIMIT from ICU, but there are two reasons not to use it. |
| // 1) ICU considers it deprecated, so by setting U_HIDE_DEPRECATED we can't see it. |
| // 2) No good reason to limit ourselves to scripts that existed in the ICU headers when |
| // WebKit was compiled. |
| // This is only really important for platforms that load an external IDN allowed script list. |
| // Not important for the compiled-in one. |
| constexpr auto scriptCodeLimit = static_cast<UScriptCode>(256); |
| |
| static uint32_t allowedIDNScriptBits[(scriptCodeLimit + 31) / 32]; |
| |
| #if !PLATFORM(COCOA) |
| |
| // Cocoa has an implementation that reads this from a file in /Library or ~/Library. |
| void loadIDNAllowedScriptList() |
| { |
| static std::once_flag flag; |
| std::call_once(flag, initializeDefaultIDNAllowedScriptList); |
| } |
| |
| #endif // !PLATFORM(COCOA) |
| |
| template<UScriptCode> bool isLookalikeCharacterOfScriptType(UChar32); |
| |
| template<> bool isLookalikeCharacterOfScriptType<USCRIPT_ARMENIAN>(UChar32 codePoint) |
| { |
| switch (codePoint) { |
| case 0x0548: /* ARMENIAN CAPITAL LETTER VO */ |
| case 0x054D: /* ARMENIAN CAPITAL LETTER SEH */ |
| case 0x0551: /* ARMENIAN CAPITAL LETTER CO */ |
| case 0x0555: /* ARMENIAN CAPITAL LETTER OH */ |
| case 0x0578: /* ARMENIAN SMALL LETTER VO */ |
| case 0x057D: /* ARMENIAN SMALL LETTER SEH */ |
| case 0x0581: /* ARMENIAN SMALL LETTER CO */ |
| case 0x0585: /* ARMENIAN SMALL LETTER OH */ |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| template<> bool isLookalikeCharacterOfScriptType<USCRIPT_TAMIL>(UChar32 codePoint) |
| { |
| switch (codePoint) { |
| case 0x0BE6: /* TAMIL DIGIT ZERO */ |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| template<> bool isLookalikeCharacterOfScriptType<USCRIPT_CANADIAN_ABORIGINAL>(UChar32 codePoint) |
| { |
| switch (codePoint) { |
| case 0x15AF: /* CANADIAN SYLLABICS AIVILIK B */ |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| template <UScriptCode ScriptType> |
| bool isOfScriptType(UChar32 codePoint) |
| { |
| UErrorCode error = U_ZERO_ERROR; |
| UScriptCode script = uscript_getScript(codePoint, &error); |
| if (error != U_ZERO_ERROR) { |
| LOG_ERROR("got ICU error while trying to look at scripts: %d", error); |
| return false; |
| } |
| return script == ScriptType; |
| } |
| |
| template<typename CharacterType> inline bool isASCIIDigitOrValidHostCharacter(CharacterType charCode) |
| { |
| if (!isASCIIDigitOrPunctuation(charCode)) |
| return false; |
| |
| // Things the URL Parser rejects: |
| switch (charCode) { |
| case '#': |
| case '%': |
| case '/': |
| case ':': |
| case '?': |
| case '@': |
| case '[': |
| case '\\': |
| case ']': |
| return false; |
| default: |
| return true; |
| } |
| } |
| |
| template <UScriptCode ScriptType> |
| bool isLookalikeSequence(const std::optional<UChar32>& previousCodePoint, UChar32 codePoint) |
| { |
| if (!previousCodePoint || *previousCodePoint == '/') |
| return false; |
| |
| auto isLookalikePair = [] (UChar first, UChar second) { |
| return isLookalikeCharacterOfScriptType<ScriptType>(first) && !(isOfScriptType<ScriptType>(second) || isASCIIDigitOrValidHostCharacter(second)); |
| }; |
| return isLookalikePair(codePoint, *previousCodePoint) |
| || isLookalikePair(*previousCodePoint, codePoint); |
| } |
| |
| static bool isLookalikeCharacter(const std::optional<UChar32>& previousCodePoint, UChar32 codePoint) |
| { |
| // This function treats the following as unsafe, lookalike characters: |
| // any non-printable character, any character considered as whitespace, |
| // any ignorable character, and emoji characters related to locks. |
| |
| // We also considered the characters in Mozilla's list of characters <http://kb.mozillazine.org/Network.IDN.blacklist_chars>. |
| |
| // Some of the characters here will never appear once ICU has encoded. |
| // For example, ICU transforms most spaces into an ASCII space and most |
| // slashes into an ASCII solidus. But one of the two callers uses this |
| // on characters that have not been processed by ICU, so they are needed here. |
| |
| if (!u_isprint(codePoint) || u_isUWhiteSpace(codePoint) || u_hasBinaryProperty(codePoint, UCHAR_DEFAULT_IGNORABLE_CODE_POINT)) |
| return true; |
| |
| switch (codePoint) { |
| case 0x00BC: /* VULGAR FRACTION ONE QUARTER */ |
| case 0x00BD: /* VULGAR FRACTION ONE HALF */ |
| case 0x00BE: /* VULGAR FRACTION THREE QUARTERS */ |
| case 0x00ED: /* LATIN SMALL LETTER I WITH ACUTE */ |
| /* 0x0131 LATIN SMALL LETTER DOTLESS I is intentionally not considered a lookalike character because it is visually distinguishable from i and it has legitimate use in the Turkish language. */ |
| case 0x01C0: /* LATIN LETTER DENTAL CLICK */ |
| case 0x01C3: /* LATIN LETTER RETROFLEX CLICK */ |
| case 0x0237: /* LATIN SMALL LETTER DOTLESS J */ |
| case 0x0251: /* LATIN SMALL LETTER ALPHA */ |
| case 0x0261: /* LATIN SMALL LETTER SCRIPT G */ |
| case 0x0274: /* LATIN LETTER SMALL CAPITAL N */ |
| case 0x027E: /* LATIN SMALL LETTER R WITH FISHHOOK */ |
| case 0x02D0: /* MODIFIER LETTER TRIANGULAR COLON */ |
| case 0x0335: /* COMBINING SHORT STROKE OVERLAY */ |
| case 0x0337: /* COMBINING SHORT SOLIDUS OVERLAY */ |
| case 0x0338: /* COMBINING LONG SOLIDUS OVERLAY */ |
| case 0x0589: /* ARMENIAN FULL STOP */ |
| case 0x05B4: /* HEBREW POINT HIRIQ */ |
| case 0x05BC: /* HEBREW POINT DAGESH OR MAPIQ */ |
| case 0x05C3: /* HEBREW PUNCTUATION SOF PASUQ */ |
| case 0x05F4: /* HEBREW PUNCTUATION GERSHAYIM */ |
| case 0x0609: /* ARABIC-INDIC PER MILLE SIGN */ |
| case 0x060A: /* ARABIC-INDIC PER TEN THOUSAND SIGN */ |
| case 0x0650: /* ARABIC KASRA */ |
| case 0x0660: /* ARABIC INDIC DIGIT ZERO */ |
| case 0x066A: /* ARABIC PERCENT SIGN */ |
| case 0x06D4: /* ARABIC FULL STOP */ |
| case 0x06F0: /* EXTENDED ARABIC INDIC DIGIT ZERO */ |
| case 0x0701: /* SYRIAC SUPRALINEAR FULL STOP */ |
| case 0x0702: /* SYRIAC SUBLINEAR FULL STOP */ |
| case 0x0703: /* SYRIAC SUPRALINEAR COLON */ |
| case 0x0704: /* SYRIAC SUBLINEAR COLON */ |
| case 0x1735: /* PHILIPPINE SINGLE PUNCTUATION */ |
| case 0x1D04: /* LATIN LETTER SMALL CAPITAL C */ |
| case 0x1D0F: /* LATIN LETTER SMALL CAPITAL O */ |
| case 0x1D1C: /* LATIN LETTER SMALL CAPITAL U */ |
| case 0x1D20: /* LATIN LETTER SMALL CAPITAL V */ |
| case 0x1D21: /* LATIN LETTER SMALL CAPITAL W */ |
| case 0x1D22: /* LATIN LETTER SMALL CAPITAL Z */ |
| case 0x1ECD: /* LATIN SMALL LETTER O WITH DOT BELOW */ |
| case 0x2010: /* HYPHEN */ |
| case 0x2011: /* NON-BREAKING HYPHEN */ |
| case 0x2024: /* ONE DOT LEADER */ |
| case 0x2027: /* HYPHENATION POINT */ |
| case 0x2039: /* SINGLE LEFT-POINTING ANGLE QUOTATION MARK */ |
| case 0x203A: /* SINGLE RIGHT-POINTING ANGLE QUOTATION MARK */ |
| case 0x2041: /* CARET INSERTION POINT */ |
| case 0x2044: /* FRACTION SLASH */ |
| case 0x2052: /* COMMERCIAL MINUS SIGN */ |
| case 0x2153: /* VULGAR FRACTION ONE THIRD */ |
| case 0x2154: /* VULGAR FRACTION TWO THIRDS */ |
| case 0x2155: /* VULGAR FRACTION ONE FIFTH */ |
| case 0x2156: /* VULGAR FRACTION TWO FIFTHS */ |
| case 0x2157: /* VULGAR FRACTION THREE FIFTHS */ |
| case 0x2158: /* VULGAR FRACTION FOUR FIFTHS */ |
| case 0x2159: /* VULGAR FRACTION ONE SIXTH */ |
| case 0x215A: /* VULGAR FRACTION FIVE SIXTHS */ |
| case 0x215B: /* VULGAR FRACTION ONE EIGHT */ |
| case 0x215C: /* VULGAR FRACTION THREE EIGHTHS */ |
| case 0x215D: /* VULGAR FRACTION FIVE EIGHTHS */ |
| case 0x215E: /* VULGAR FRACTION SEVEN EIGHTHS */ |
| case 0x215F: /* FRACTION NUMERATOR ONE */ |
| case 0x2212: /* MINUS SIGN */ |
| case 0x2215: /* DIVISION SLASH */ |
| case 0x2216: /* SET MINUS */ |
| case 0x2236: /* RATIO */ |
| case 0x233F: /* APL FUNCTIONAL SYMBOL SLASH BAR */ |
| case 0x23AE: /* INTEGRAL EXTENSION */ |
| case 0x244A: /* OCR DOUBLE BACKSLASH */ |
| case 0x2571: /* BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT */ |
| case 0x2572: /* BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT */ |
| case 0x29F6: /* SOLIDUS WITH OVERBAR */ |
| case 0x29F8: /* BIG SOLIDUS */ |
| case 0x2AFB: /* TRIPLE SOLIDUS BINARY RELATION */ |
| case 0x2AFD: /* DOUBLE SOLIDUS OPERATOR */ |
| case 0x2FF0: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT */ |
| case 0x2FF1: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO BELOW */ |
| case 0x2FF2: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO MIDDLE AND RIGHT */ |
| case 0x2FF3: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO MIDDLE AND BELOW */ |
| case 0x2FF4: /* IDEOGRAPHIC DESCRIPTION CHARACTER FULL SURROUND */ |
| case 0x2FF5: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM ABOVE */ |
| case 0x2FF6: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM BELOW */ |
| case 0x2FF7: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LEFT */ |
| case 0x2FF8: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER LEFT */ |
| case 0x2FF9: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER RIGHT */ |
| case 0x2FFA: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LOWER LEFT */ |
| case 0x2FFB: /* IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID */ |
| case 0x3002: /* IDEOGRAPHIC FULL STOP */ |
| case 0x3008: /* LEFT ANGLE BRACKET */ |
| case 0x3014: /* LEFT TORTOISE SHELL BRACKET */ |
| case 0x3015: /* RIGHT TORTOISE SHELL BRACKET */ |
| case 0x3033: /* VERTICAL KANA REPEAT MARK UPPER HALF */ |
| case 0x3035: /* VERTICAL KANA REPEAT MARK LOWER HALF */ |
| case 0x321D: /* PARENTHESIZED KOREAN CHARACTER OJEON */ |
| case 0x321E: /* PARENTHESIZED KOREAN CHARACTER O HU */ |
| case 0x33AE: /* SQUARE RAD OVER S */ |
| case 0x33AF: /* SQUARE RAD OVER S SQUARED */ |
| case 0x33C6: /* SQUARE C OVER KG */ |
| case 0x33DF: /* SQUARE A OVER M */ |
| case 0x05B9: /* HEBREW POINT HOLAM */ |
| case 0x05BA: /* HEBREW POINT HOLAM HASER FOR VAV */ |
| case 0x05C1: /* HEBREW POINT SHIN DOT */ |
| case 0x05C2: /* HEBREW POINT SIN DOT */ |
| case 0x05C4: /* HEBREW MARK UPPER DOT */ |
| case 0xA731: /* LATIN LETTER SMALL CAPITAL S */ |
| case 0xA771: /* LATIN SMALL LETTER DUM */ |
| case 0xA789: /* MODIFIER LETTER COLON */ |
| case 0xFE14: /* PRESENTATION FORM FOR VERTICAL SEMICOLON */ |
| case 0xFE15: /* PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK */ |
| case 0xFE3F: /* PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET */ |
| case 0xFE5D: /* SMALL LEFT TORTOISE SHELL BRACKET */ |
| case 0xFE5E: /* SMALL RIGHT TORTOISE SHELL BRACKET */ |
| case 0xFF0E: /* FULLWIDTH FULL STOP */ |
| case 0xFF0F: /* FULL WIDTH SOLIDUS */ |
| case 0xFF61: /* HALFWIDTH IDEOGRAPHIC FULL STOP */ |
| case 0xFFFC: /* OBJECT REPLACEMENT CHARACTER */ |
| case 0xFFFD: /* REPLACEMENT CHARACTER */ |
| case 0x1F50F: /* LOCK WITH INK PEN */ |
| case 0x1F510: /* CLOSED LOCK WITH KEY */ |
| case 0x1F511: /* KEY */ |
| case 0x1F512: /* LOCK */ |
| case 0x1F513: /* OPEN LOCK */ |
| return true; |
| case 0x0307: /* COMBINING DOT ABOVE */ |
| return previousCodePoint == 0x0237 /* LATIN SMALL LETTER DOTLESS J */ |
| || previousCodePoint == 0x0131 /* LATIN SMALL LETTER DOTLESS I */ |
| || previousCodePoint == 0x05D5; /* HEBREW LETTER VAV */ |
| case '.': |
| return false; |
| default: |
| return isLookalikeSequence<USCRIPT_ARMENIAN>(previousCodePoint, codePoint) |
| || isLookalikeSequence<USCRIPT_TAMIL>(previousCodePoint, codePoint) |
| || isLookalikeSequence<USCRIPT_CANADIAN_ABORIGINAL>(previousCodePoint, codePoint); |
| } |
| } |
| |
| static void addScriptToIDNAllowedScriptList(int32_t script) |
| { |
| if (script >= 0 && script < scriptCodeLimit) { |
| size_t index = script / 32; |
| uint32_t mask = 1 << (script % 32); |
| allowedIDNScriptBits[index] |= mask; |
| } |
| } |
| |
| static void addScriptToIDNAllowedScriptList(UScriptCode script) |
| { |
| addScriptToIDNAllowedScriptList(static_cast<int32_t>(script)); |
| } |
| |
| void addScriptToIDNAllowedScriptList(const char* scriptName) |
| { |
| addScriptToIDNAllowedScriptList(u_getPropertyValueEnum(UCHAR_SCRIPT, scriptName)); |
| } |
| |
| void initializeDefaultIDNAllowedScriptList() |
| { |
| constexpr UScriptCode scripts[] = { |
| USCRIPT_COMMON, |
| USCRIPT_INHERITED, |
| USCRIPT_ARABIC, |
| USCRIPT_ARMENIAN, |
| USCRIPT_BOPOMOFO, |
| USCRIPT_CANADIAN_ABORIGINAL, |
| USCRIPT_DEVANAGARI, |
| USCRIPT_DESERET, |
| USCRIPT_GUJARATI, |
| USCRIPT_GURMUKHI, |
| USCRIPT_HANGUL, |
| USCRIPT_HAN, |
| USCRIPT_HEBREW, |
| USCRIPT_HIRAGANA, |
| USCRIPT_KATAKANA_OR_HIRAGANA, |
| USCRIPT_KATAKANA, |
| USCRIPT_LATIN, |
| USCRIPT_TAMIL, |
| USCRIPT_THAI, |
| USCRIPT_YI, |
| }; |
| for (auto script : scripts) |
| addScriptToIDNAllowedScriptList(script); |
| } |
| |
| static bool allCharactersInAllowedIDNScriptList(const UChar* buffer, int32_t length) |
| { |
| loadIDNAllowedScriptList(); |
| int32_t i = 0; |
| std::optional<UChar32> previousCodePoint; |
| while (i < length) { |
| UChar32 c; |
| U16_NEXT(buffer, i, length, c); |
| UErrorCode error = U_ZERO_ERROR; |
| UScriptCode script = uscript_getScript(c, &error); |
| if (error != U_ZERO_ERROR) { |
| LOG_ERROR("got ICU error while trying to look at scripts: %d", error); |
| return false; |
| } |
| if (script < 0) { |
| LOG_ERROR("got negative number for script code from ICU: %d", script); |
| return false; |
| } |
| if (script >= scriptCodeLimit) |
| return false; |
| |
| size_t index = script / 32; |
| uint32_t mask = 1 << (script % 32); |
| if (!(allowedIDNScriptBits[index] & mask)) |
| return false; |
| |
| if (isLookalikeCharacter(previousCodePoint, c)) |
| return false; |
| previousCodePoint = c; |
| } |
| return true; |
| } |
| |
| template<typename Func> |
| static inline bool isSecondLevelDomainNameAllowedByTLDRules(const UChar* buffer, int32_t length, Func characterIsAllowed) |
| { |
| ASSERT(length > 0); |
| |
| for (int32_t i = length - 1; i >= 0; --i) { |
| UChar ch = buffer[i]; |
| |
| if (characterIsAllowed(ch)) |
| continue; |
| |
| // Only check the second level domain. Lower level registrars may have different rules. |
| if (ch == '.') |
| break; |
| |
| return false; |
| } |
| return true; |
| } |
| |
| #define CHECK_RULES_IF_SUFFIX_MATCHES(suffix, function) \ |
| { \ |
| static const int32_t suffixLength = sizeof(suffix) / sizeof(suffix[0]); \ |
| if (length > suffixLength && !memcmp(buffer + length - suffixLength, suffix, sizeof(suffix))) \ |
| return isSecondLevelDomainNameAllowedByTLDRules(buffer, length - suffixLength, function); \ |
| } |
| |
| static bool isRussianDomainNameCharacter(UChar ch) |
| { |
| // Only modern Russian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || isASCIIDigit(ch) || ch == '-'; |
| } |
| |
| static bool allCharactersAllowedByTLDRules(const UChar* buffer, int32_t length) |
| { |
| // Skip trailing dot for root domain. |
| if (buffer[length - 1] == '.') |
| length--; |
| |
| // http://cctld.ru/files/pdf/docs/rules_ru-rf.pdf |
| static const UChar cyrillicRF[] = { |
| '.', |
| 0x0440, // CYRILLIC SMALL LETTER ER |
| 0x0444, // CYRILLIC SMALL LETTER EF |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRF, isRussianDomainNameCharacter); |
| |
| // http://rusnames.ru/rules.pl |
| static const UChar cyrillicRUS[] = { |
| '.', |
| 0x0440, // CYRILLIC SMALL LETTER ER |
| 0x0443, // CYRILLIC SMALL LETTER U |
| 0x0441, // CYRILLIC SMALL LETTER ES |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRUS, isRussianDomainNameCharacter); |
| |
| // http://ru.faitid.org/projects/moscow/documents/moskva/idn |
| static const UChar cyrillicMOSKVA[] = { |
| '.', |
| 0x043C, // CYRILLIC SMALL LETTER EM |
| 0x043E, // CYRILLIC SMALL LETTER O |
| 0x0441, // CYRILLIC SMALL LETTER ES |
| 0x043A, // CYRILLIC SMALL LETTER KA |
| 0x0432, // CYRILLIC SMALL LETTER VE |
| 0x0430, // CYRILLIC SMALL LETTER A |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMOSKVA, isRussianDomainNameCharacter); |
| |
| // http://www.dotdeti.ru/foruser/docs/regrules.php |
| static const UChar cyrillicDETI[] = { |
| '.', |
| 0x0434, // CYRILLIC SMALL LETTER DE |
| 0x0435, // CYRILLIC SMALL LETTER IE |
| 0x0442, // CYRILLIC SMALL LETTER TE |
| 0x0438, // CYRILLIC SMALL LETTER I |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicDETI, isRussianDomainNameCharacter); |
| |
| // http://corenic.org - rules not published. The word is Russian, so only allowing Russian at this time, |
| // although we may need to revise the checks if this ends up being used with other languages spoken in Russia. |
| static const UChar cyrillicONLAYN[] = { |
| '.', |
| 0x043E, // CYRILLIC SMALL LETTER O |
| 0x043D, // CYRILLIC SMALL LETTER EN |
| 0x043B, // CYRILLIC SMALL LETTER EL |
| 0x0430, // CYRILLIC SMALL LETTER A |
| 0x0439, // CYRILLIC SMALL LETTER SHORT I |
| 0x043D, // CYRILLIC SMALL LETTER EN |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicONLAYN, isRussianDomainNameCharacter); |
| |
| // http://corenic.org - same as above. |
| static const UChar cyrillicSAYT[] = { |
| '.', |
| 0x0441, // CYRILLIC SMALL LETTER ES |
| 0x0430, // CYRILLIC SMALL LETTER A |
| 0x0439, // CYRILLIC SMALL LETTER SHORT I |
| 0x0442, // CYRILLIC SMALL LETTER TE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSAYT, isRussianDomainNameCharacter); |
| |
| // http://pir.org/products/opr-domain/ - rules not published. According to the registry site, |
| // the intended audience is "Russian and other Slavic-speaking markets". |
| // Chrome appears to only allow Russian, so sticking with that for now. |
| static const UChar cyrillicORG[] = { |
| '.', |
| 0x043E, // CYRILLIC SMALL LETTER O |
| 0x0440, // CYRILLIC SMALL LETTER ER |
| 0x0433, // CYRILLIC SMALL LETTER GHE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicORG, isRussianDomainNameCharacter); |
| |
| // http://cctld.by/rules.html |
| static const UChar cyrillicBEL[] = { |
| '.', |
| 0x0431, // CYRILLIC SMALL LETTER BE |
| 0x0435, // CYRILLIC SMALL LETTER IE |
| 0x043B, // CYRILLIC SMALL LETTER EL |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicBEL, [](UChar ch) { |
| // Russian and Byelorussian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0456 || ch == 0x045E || ch == 0x2019 || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // http://www.nic.kz/docs/poryadok_vnedreniya_kaz_ru.pdf |
| static const UChar cyrillicKAZ[] = { |
| '.', |
| 0x049B, // CYRILLIC SMALL LETTER KA WITH DESCENDER |
| 0x0430, // CYRILLIC SMALL LETTER A |
| 0x0437, // CYRILLIC SMALL LETTER ZE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicKAZ, [](UChar ch) { |
| // Kazakh letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x04D9 || ch == 0x0493 || ch == 0x049B || ch == 0x04A3 || ch == 0x04E9 || ch == 0x04B1 || ch == 0x04AF || ch == 0x04BB || ch == 0x0456 || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // http://uanic.net/docs/documents-ukr/Rules%20of%20UKR_v4.0.pdf |
| static const UChar cyrillicUKR[] = { |
| '.', |
| 0x0443, // CYRILLIC SMALL LETTER U |
| 0x043A, // CYRILLIC SMALL LETTER KA |
| 0x0440, // CYRILLIC SMALL LETTER ER |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicUKR, [](UChar ch) { |
| // Russian and Ukrainian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0491 || ch == 0x0404 || ch == 0x0456 || ch == 0x0457 || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // http://www.rnids.rs/data/DOKUMENTI/idn-srb-policy-termsofuse-v1.4-eng.pdf |
| static const UChar cyrillicSRB[] = { |
| '.', |
| 0x0441, // CYRILLIC SMALL LETTER ES |
| 0x0440, // CYRILLIC SMALL LETTER ER |
| 0x0431, // CYRILLIC SMALL LETTER BE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSRB, [](UChar ch) { |
| // Serbian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x0438) || (ch >= 0x043A && ch <= 0x0448) || ch == 0x0452 || ch == 0x0458 || ch == 0x0459 || ch == 0x045A || ch == 0x045B || ch == 0x045F || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // http://marnet.mk/doc/pravilnik-mk-mkd.pdf |
| static const UChar cyrillicMKD[] = { |
| '.', |
| 0x043C, // CYRILLIC SMALL LETTER EM |
| 0x043A, // CYRILLIC SMALL LETTER KA |
| 0x0434, // CYRILLIC SMALL LETTER DE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMKD, [](UChar ch) { |
| // Macedonian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x0438) || (ch >= 0x043A && ch <= 0x0448) || ch == 0x0453 || ch == 0x0455 || ch == 0x0458 || ch == 0x0459 || ch == 0x045A || ch == 0x045C || ch == 0x045F || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // https://www.mon.mn/cs/ |
| static const UChar cyrillicMON[] = { |
| '.', |
| 0x043C, // CYRILLIC SMALL LETTER EM |
| 0x043E, // CYRILLIC SMALL LETTER O |
| 0x043D, // CYRILLIC SMALL LETTER EN |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMON, [](UChar ch) { |
| // Mongolian letters, digits and dashes are allowed. |
| return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x04E9 || ch == 0x04AF || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // https://www.icann.org/sites/default/files/packages/lgr/lgr-second-level-bulgarian-30aug16-en.html |
| static const UChar cyrillicBG[] = { |
| '.', |
| 0x0431, // CYRILLIC SMALL LETTER BE |
| 0x0433 // CYRILLIC SMALL LETTER GHE |
| }; |
| CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicBG, [](UChar ch) { |
| return (ch >= 0x0430 && ch <= 0x044A) || ch == 0x044C || (ch >= 0x044E && ch <= 0x0450) || ch == 0x045D || isASCIIDigit(ch) || ch == '-'; |
| }); |
| |
| // Not a known top level domain with special rules. |
| return false; |
| } |
| |
| // Return value of null means no mapping is necessary. |
| std::optional<String> mapHostName(const String& hostName, URLDecodeFunction decodeFunction) |
| { |
| // destinationBuffer needs to be big enough to hold an IDN-encoded name. |
| // For host names bigger than this, we won't do IDN encoding, which is almost certainly OK. |
| if (hostName.length() > URLParser::hostnameBufferLength) |
| return String(); |
| |
| if (!hostName.length()) |
| return String(); |
| |
| String string; |
| if (decodeFunction && string.contains('%')) |
| string = (*decodeFunction)(hostName); |
| else |
| string = hostName; |
| |
| unsigned length = string.length(); |
| |
| auto sourceBuffer = string.charactersWithNullTermination(); |
| |
| UChar destinationBuffer[URLParser::hostnameBufferLength]; |
| UErrorCode uerror = U_ZERO_ERROR; |
| UIDNAInfo processingDetails = UIDNA_INFO_INITIALIZER; |
| int32_t numCharactersConverted = (decodeFunction ? uidna_nameToASCII : uidna_nameToUnicode)(&URLParser::internationalDomainNameTranscoder(), sourceBuffer.data(), length, destinationBuffer, URLParser::hostnameBufferLength, &processingDetails, &uerror); |
| int allowedErrors = decodeFunction ? 0 : URLParser::allowedNameToASCIIErrors; |
| if (length && (U_FAILURE(uerror) || processingDetails.errors & ~allowedErrors)) |
| return std::nullopt; |
| |
| if (numCharactersConverted == static_cast<int32_t>(length) && !memcmp(sourceBuffer.data(), destinationBuffer, length * sizeof(UChar))) |
| return String(); |
| |
| if (!decodeFunction && !allCharactersInAllowedIDNScriptList(destinationBuffer, numCharactersConverted) && !allCharactersAllowedByTLDRules(destinationBuffer, numCharactersConverted)) |
| return String(); |
| |
| return String(destinationBuffer, numCharactersConverted); |
| } |
| |
| using MappingRangesVector = std::optional<Vector<std::tuple<unsigned, unsigned, String>>>; |
| |
| static void collectRangesThatNeedMapping(const String& string, unsigned location, unsigned length, MappingRangesVector& array, URLDecodeFunction decodeFunction) |
| { |
| // Generally, we want to optimize for the case where there is one host name that does not need mapping. |
| // Therefore, we use null to indicate no mapping here and an empty array to indicate error. |
| |
| String substring = string.substringSharingImpl(location, length); |
| std::optional<String> host = mapHostName(substring, decodeFunction); |
| |
| if (host && !*host) |
| return; |
| |
| if (!array) |
| array = Vector<std::tuple<unsigned, unsigned, String>>(); |
| |
| if (host) |
| array->constructAndAppend(location, length, *host); |
| } |
| |
| static void applyHostNameFunctionToMailToURLString(const String& string, URLDecodeFunction decodeFunction, MappingRangesVector& array) |
| { |
| // In a mailto: URL, host names come after a '@' character and end with a '>' or ',' or '?' character. |
| // Skip quoted strings so that characters in them don't confuse us. |
| // When we find a '?' character, we are past the part of the URL that contains host names. |
| |
| unsigned stringLength = string.length(); |
| unsigned current = 0; |
| |
| while (1) { |
| // Find start of host name or of quoted string. |
| auto hostNameOrStringStart = string.find([](UChar ch) { |
| return ch == '"' || ch == '@' || ch == '?'; |
| }, current); |
| if (hostNameOrStringStart == notFound) |
| return; |
| |
| UChar c = string[hostNameOrStringStart]; |
| current = hostNameOrStringStart + 1; |
| |
| if (c == '?') |
| return; |
| |
| if (c == '@') { |
| // Find end of host name. |
| unsigned hostNameStart = current; |
| auto hostNameEnd = string.find([](UChar ch) { |
| return ch == '>' || ch == ',' || ch == '?'; |
| }, current); |
| |
| bool done; |
| if (hostNameEnd == notFound) { |
| hostNameEnd = stringLength; |
| done = true; |
| } else { |
| current = hostNameEnd; |
| done = false; |
| } |
| |
| // Process host name range. |
| collectRangesThatNeedMapping(string, hostNameStart, hostNameEnd - hostNameStart, array, decodeFunction); |
| |
| if (done) |
| return; |
| } else { |
| // Skip quoted string. |
| ASSERT(c == '"'); |
| while (1) { |
| auto escapedCharacterOrStringEnd = string.find([](UChar ch) { |
| return ch == '"' || ch == '\\'; |
| }, current); |
| if (escapedCharacterOrStringEnd == notFound) |
| return; |
| |
| c = string[escapedCharacterOrStringEnd]; |
| current = escapedCharacterOrStringEnd + 1; |
| |
| // If we are the end of the string, then break from the string loop back to the host name loop. |
| if (c == '"') |
| break; |
| |
| // Skip escaped character. |
| ASSERT(c == '\\'); |
| if (current == stringLength) |
| return; |
| |
| ++current; |
| } |
| } |
| } |
| } |
| |
| static void applyHostNameFunctionToURLString(const String& string, URLDecodeFunction decodeFunction, MappingRangesVector& array) |
| { |
| // Find hostnames. Too bad we can't use any real URL-parsing code to do this, |
| // but we have to do it before doing all the %-escaping, and this is the only |
| // code we have that parses mailto URLs anyway. |
| |
| // Maybe we should implement this using a character buffer instead? |
| |
| if (protocolIs(string, "mailto"_s)) { |
| applyHostNameFunctionToMailToURLString(string, decodeFunction, array); |
| return; |
| } |
| |
| // Find the host name in a hierarchical URL. |
| // It comes after a "://" sequence, with scheme characters preceding. |
| // If ends with the end of the string or a ":", "/", or a "?". |
| // If there is a "@" character, the host part is just the part after the "@". |
| static constexpr auto separator = "://"_s; |
| auto separatorIndex = string.find(separator); |
| if (separatorIndex == notFound) |
| return; |
| |
| unsigned authorityStart = separatorIndex + separator.length(); |
| |
| // Check that all characters before the :// are valid scheme characters. |
| if (StringView { string }.left(separatorIndex).contains([](UChar character) { |
| return !(isASCIIAlphanumeric(character) || character == '+' || character == '-' || character == '.'); |
| })) |
| return; |
| |
| // Find terminating character. |
| auto hostNameTerminator = string.find([](UChar character) { |
| return character == ':' || character == '/' || character == '?' || character == '#'; |
| }, authorityStart); |
| unsigned hostNameEnd = hostNameTerminator == notFound ? string.length() : hostNameTerminator; |
| |
| // Find "@" for the start of the host name. |
| auto userInfoTerminator = StringView { string }.left(hostNameEnd).find('@', authorityStart); |
| unsigned hostNameStart = userInfoTerminator == notFound ? authorityStart : userInfoTerminator + 1; |
| |
| collectRangesThatNeedMapping(string, hostNameStart, hostNameEnd - hostNameStart, array, decodeFunction); |
| } |
| |
| String mapHostNames(const String& string, URLDecodeFunction decodeFunction) |
| { |
| // Generally, we want to optimize for the case where there is one host name that does not need mapping. |
| |
| if (decodeFunction && string.isAllASCII()) |
| return string; |
| |
| // Make a list of ranges that actually need mapping. |
| MappingRangesVector hostNameRanges; |
| applyHostNameFunctionToURLString(string, decodeFunction, hostNameRanges); |
| if (!hostNameRanges) |
| return string; |
| |
| if (hostNameRanges->isEmpty()) |
| return { }; |
| |
| // Do the mapping. |
| String result = string; |
| while (!hostNameRanges->isEmpty()) { |
| auto [location, length, mappedHostName] = hostNameRanges->takeLast(); |
| result = makeStringByReplacing(result, location, length, mappedHostName); |
| } |
| return result; |
| } |
| |
| static String escapeUnsafeCharacters(const String& sourceBuffer) |
| { |
| unsigned length = sourceBuffer.length(); |
| |
| std::optional<UChar32> previousCodePoint; |
| |
| unsigned i; |
| for (i = 0; i < length; ) { |
| UChar32 c = sourceBuffer.characterStartingAt(i); |
| if (isLookalikeCharacter(previousCodePoint, sourceBuffer.characterStartingAt(i))) |
| break; |
| previousCodePoint = c; |
| i += U16_LENGTH(c); |
| } |
| |
| if (i == length) |
| return sourceBuffer; |
| |
| Vector<UChar, urlBytesBufferLength> outBuffer; |
| |
| outBuffer.grow(i); |
| if (sourceBuffer.is8Bit()) |
| StringImpl::copyCharacters(outBuffer.data(), sourceBuffer.characters8(), i); |
| else |
| StringImpl::copyCharacters(outBuffer.data(), sourceBuffer.characters16(), i); |
| |
| for (; i < length; ) { |
| UChar32 c = sourceBuffer.characterStartingAt(i); |
| unsigned characterLength = U16_LENGTH(c); |
| if (isLookalikeCharacter(previousCodePoint, c)) { |
| uint8_t utf8Buffer[4]; |
| size_t offset = 0; |
| UBool failure = false; |
| U8_APPEND(utf8Buffer, offset, 4, c, failure); |
| ASSERT_UNUSED(failure, !failure); |
| |
| for (size_t j = 0; j < offset; ++j) { |
| outBuffer.append('%'); |
| outBuffer.append(upperNibbleToASCIIHexDigit(utf8Buffer[j])); |
| outBuffer.append(lowerNibbleToASCIIHexDigit(utf8Buffer[j])); |
| } |
| } else { |
| for (unsigned j = 0; j < characterLength; ++j) |
| outBuffer.append(sourceBuffer[i + j]); |
| } |
| previousCodePoint = c; |
| i += characterLength; |
| } |
| |
| return String::adopt(WTFMove(outBuffer)); |
| } |
| |
| String userVisibleURL(const CString& url) |
| { |
| auto* before = url.dataAsUInt8Ptr(); |
| int length = url.length(); |
| |
| if (!length) |
| return { }; |
| |
| bool mayNeedHostNameDecoding = false; |
| |
| CheckedInt32 bufferLength = length; |
| bufferLength = bufferLength * 3 + 1; // The buffer should be large enough to %-escape every character. |
| if (bufferLength.hasOverflowed()) |
| return { }; |
| Vector<char, urlBytesBufferLength> after(bufferLength); |
| |
| char* q = after.data(); |
| { |
| const unsigned char* p = before; |
| for (int i = 0; i < length; i++) { |
| unsigned char c = p[i]; |
| // unescape escape sequences that indicate bytes greater than 0x7f |
| if (c == '%' && i + 2 < length && isASCIIHexDigit(p[i + 1]) && isASCIIHexDigit(p[i + 2])) { |
| auto u = toASCIIHexValue(p[i + 1], p[i + 2]); |
| if (u > 0x7f) { |
| // unescape |
| *q++ = u; |
| } else { |
| // do not unescape |
| *q++ = p[i]; |
| *q++ = p[i + 1]; |
| *q++ = p[i + 2]; |
| } |
| i += 2; |
| } else { |
| *q++ = c; |
| |
| // Check for "xn--" in an efficient, non-case-sensitive, way. |
| if (c == '-' && i >= 3 && !mayNeedHostNameDecoding && (q[-4] | 0x20) == 'x' && (q[-3] | 0x20) == 'n' && q[-2] == '-') |
| mayNeedHostNameDecoding = true; |
| } |
| } |
| *q = '\0'; |
| } |
| |
| // Check string to see if it can be converted to display using UTF-8 |
| String result = String::fromUTF8(after.data()); |
| if (!result) { |
| // Could not convert to UTF-8. |
| // Convert characters greater than 0x7f to escape sequences. |
| // Shift current string to the end of the buffer |
| // then we will copy back bytes to the start of the buffer |
| // as we convert. |
| int afterlength = q - after.data(); |
| char* p = after.data() + bufferLength.value() - afterlength - 1; |
| memmove(p, after.data(), afterlength + 1); // copies trailing '\0' |
| char* q = after.data(); |
| while (*p) { |
| unsigned char c = *p; |
| if (c > 0x7f) { |
| *q++ = '%'; |
| *q++ = upperNibbleToASCIIHexDigit(c); |
| *q++ = lowerNibbleToASCIIHexDigit(c); |
| } else |
| *q++ = *p; |
| p++; |
| } |
| *q = '\0'; |
| // Note: after.data() points to a null-terminated, pure ASCII string. |
| result = String::fromUTF8(after.data()); |
| ASSERT(!!result); |
| } |
| |
| // Note: result is UTF–16 string, created from either a valid UTF-8 string, |
| // or a pure ASCII string (where all bytes with the high bit set are |
| // percent-encoded). |
| |
| if (mayNeedHostNameDecoding) { |
| // FIXME: Is it good to ignore the failure of mapHostNames and keep result intact? |
| auto mappedResult = mapHostNames(result, nullptr); |
| if (!!mappedResult) |
| result = mappedResult; |
| } |
| |
| return escapeUnsafeCharacters(normalizedNFC(result)); |
| } |
| |
| } // namespace URLHelpers |
| } // namespace WTF |