【问题标题】:How to check if a domain is punycode or not?如何检查域是否是punycode?
【发布时间】:2016-05-13 11:44:29
【问题描述】:

好吧,我使用 idna_convert PHP 类 (http://idnaconv.net/index.html) 来编码/解码域名。

不幸的是,它似乎没有提供一个接口来检查一个域名是否已经是 punycode。

实现这一目标的最佳方法是什么?如果有人可以发布源代码如何验证域是否是 punycode,那就太好了(有解释,因为 idna_convert 代码对我来说不是很清楚)。我已经知道如何从 idna_convert 捕获异常。 :-)

顺便说一句:当您尝试将域名转换为已经是 punycode 的 punycode 时,idna_convert 会引发异常(请参阅https://github.com/phlylabs/idna-convert/blob/master/src/Punycode.php;第 157 行)。此外,我真的不明白他们的支票是如何运作的。

【问题讨论】:

  • 可以尝试php idn_to_utf8 函数并将输出与输入进行比较? php.net/manual/en/function.idn-to-utf8.php
  • @PavelPetrov:谢谢,这个函数看起来很有趣,而且比捕获异常要好得多。 :-)
  • @Andreas 但它可能会产生错误的结果,因为 punycode 不仅转换为 unicode。否则,将不需要 idna_convert,你知道的。
  • 你总是可以从库中删除有问题的代码。引发异常的代码完全没有必要,应该删除。解码器不检查域是否已经解码,因此该库在内部与自身不一致。或者只是写你自己的。 RFC3492 中有一些相当简陋的示例 C 代码,很容易移植。 That's what I did。只需约 300 行代码即可避免对第三方的不必要依赖。

标签: php punycode


【解决方案1】:

最简单的方法 - 无论如何转换它并检查结果是否等于输入。

编辑:您可以通过这样的检查来扩展 Punycode 类:

class PunycodeCheck extends Punycode
{
  public function check_encoded($decoded)
  {
      $extract = self::byteLength(self::punycodePrefix);
      $check_pref = $this->UnicodeTranscoder->utf8_ucs4array(self::punycodePrefix);
      $check_deco = array_slice($decoded, 0, $extract);
      if ($check_pref == $check_deco) 
          return true;
      return false;
   }
}

【讨论】:

  • 这是一个很好的建议,但不幸的是它不起作用,因为当域已经是 punycode 并且您尝试对其进行编码时,idna_convert 会引发异常。请参阅github.com/phlylabs/idna-convert/blob/master/src/Punycode.php(第 157 行)。
  • @Andreas 然后抓住这个异常并检查异常文本 - 你没事!
  • 是的,但我不认为这真的是一个有效的用法......我认为首先检查域是否需要编码更有意义(并且更直接)到 punycode 或者如果它已经是。所以我知道捕获异常是解决问题的一种方法,但我不是很喜欢这种方式...
  • @Andreas 检查域是否需要编码的唯一有效方法是尝试编码。如果你不喜欢捕获异常,你可以通过扩展 Punycode 类来添加你自己的函数,该函数返回一些已编码域的特殊结果,而不是抛出异常。
  • 嗯,我知道,但这正是我想要的?有关如何最好地验证域是否为 punycode 的示例代码(带说明)。我知道 OOP 以及如何捕获异常、扩展类等;-)
【解决方案2】:

这取决于你到底想要什么。

作为第一个基本检查,查看域名是否仅包含 ASCII 字符。如果是,那么该域是“已经是 punycode”,在某种意义上它不能被进一步转换。检查字符串是否仅包含 ASCII 字符,请参阅Determine if UTF-8 text is all ASCII?

如果最重要的是,您想检查域是否在 IDN 表单中,在点 . 处拆分域并检查是否有任何子字符串以 xn-- 开头。

如果除此之外,您还想检查域是否为 IDN 且有效,只需尝试使用库的 decode 函数对其进行解码。

【讨论】:

    【解决方案3】:

    要检查一个域是否在 Punycode 中并不容易。需要通过@Wladston 已经说过的规则来实施几个检查。

    这是我从 ValidateHelper 类从我的库组合中获取的改编代码示例:Helper classes for PrestaShop CMS。我还添加了测试及其执行结果。

    /**
     * Validate helper.
     *
     * @author Maksim T. <zapalm@yandex.com>
     */
    class ValidateHelper
    {
        /**
         * Checks if the given domain is in Punycode.
         *
         * @param string $domain The domain to check.
         *
         * @return bool Whether the domain is in Punycode.
         *
         * @see https://developer.mozilla.org/en-US/docs/Mozilla/Internationalized_domain_names_support_in_Mozilla#ASCII-compatible_encoding_.28ACE.29
         *
         * @author Maksim T. <zapalm@yandex.com>
         */
        public static function isPunycodeDomain($domain)
        {
            $hasPunycode = false;
    
            foreach (explode('.', $domain) as $part) {
                if (false === static::isAscii($part)) {
                    return false;
                }
    
                if (static::isPunycode($part)) {
                    $hasPunycode = true;
                }
            }
    
            return $hasPunycode;
        }
    
        /**
         * Checks if the given value is in ASCII character encoding.
         *
         * @param string $value The value to check.
         *
         * @return bool Whether the value is in ASCII character encoding.
         *
         * @see https://en.wikipedia.org/wiki/ASCII
         *
         * @author Maksim T. <zapalm@yandex.com>
         */
        public static function isAscii($value)
        {
            return ('ASCII' === mb_detect_encoding($value, 'ASCII', true));
        }
    
        /**
         * Checks if the given value is in Punycode.
         *
         * @param string $value The value to check.
         *
         * @return bool Whether the value is in Punycode.
         *
         * @throws \LogicException If the string is not encoded by UTF-8.
         *
         * @see https://en.wikipedia.org/wiki/Punycode
         *
         * @author Maksim T. <zapalm@yandex.com>
         */
        public static function isPunycode($value)
        {
            if (false === static::isAscii($value)) {
                return false;
            }
    
            if ('UTF-8' !== mb_detect_encoding($value, 'UTF-8', true)) {
                throw new \LogicException('The string should be encoded by UTF-8 to do the right check.');
            }
    
            return (0 === mb_stripos($value, 'xn--', 0, 'UTF-8'));
        }
    }
    
    /**
     * Test Punycode domain validator.
     *
     * @author Maksim T. <zapalm@yandex.com>
     */
    class Test
    {
        /**
         * Run the test.
         *
         * @author Maksim T. <zapalm@yandex.com>
         */
        public static function run()
        {
            $domains = [
                // White list
                'почта@престашоп.рф'          => false, // Russian, Unicode
                'modulez.ru'                  => false, // English, ASCII
                'xn--80aj2abdcii9c.xn--p1ai'  => true,  // Russian, ASCII
                'xn--80a1acn3a.xn--j1amh'     => true,  // Ukrainian, ASCII
                'xn--srensen-90a.example.com' => true,  // German, ASCII
                'xn--mxahbxey0c.xn--xxaf0a'   => true,  // Greek, ASCII
                'xn--fsqu00a.xn--4rr70v'      => true,  // Chinese, ASCII
    
                // Black List
                'xn--престашоп.xn--рф'        => false, // Russian, Unicode
                'xn--prestashop.рф'           => false, // Russian, Unicode
            ];
    
            foreach ($domains as $domain => $isPunycode) {
                echo 'TEST: ' . $domain . (ValidateHelper::isPunycodeDomain($domain)
                    ? ' is in Punycode [' . ($isPunycode ? 'OK' : 'FAIL') . ']'
                    : ' is NOT in Punycode [' . (false === $isPunycode ? 'OK' : 'FAIL') . ']'
                ) . PHP_EOL;
            }
        }
    }
    
    Test::run();
    
    // The output result:
    //
    // TEST: почта@престашоп.рф is NOT in Punycode [OK]
    // TEST: modulez.ru is NOT in Punycode [OK]
    // TEST: xn--80aj2abdcii9c.xn--p1ai is in Punycode [OK]
    // TEST: xn--80a1acn3a.xn--j1amh is in Punycode [OK]
    // TEST: xn--srensen-90a.example.com is in Punycode [OK]
    // TEST: xn--mxahbxey0c.xn--xxaf0a is in Punycode [OK]
    // TEST: xn--fsqu00a.xn--4rr70v is in Punycode [OK]
    // TEST: xn--престашоп.xn--рф is NOT in Punycode [OK]
    // TEST: xn--prestashop.рф is NOT in Punycode [OK]
    

    【讨论】:

      【解决方案4】:

      encode() 方法抛出的唯一异常是域已经是 punycode。因此,您可以执行以下操作:

      try {
          $punycode->encode($decoded);
      } catch (\InvalidArgumentException $e) {
          //do whatever is needed when already punycode
          //or do nothing
      }
      

      不过,这是一种变通的解决方案。

      【讨论】:

      • 我同意,但在我看来,如果该域已经是 punycode 或不是,最好提前检查。捕获 InvalidArgumentException 似乎相当......好吧,很脏。
      • 我同意,这只是解决问题的第一件事。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-12-24
      • 2015-02-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-18
      相关资源
      最近更新 更多