Imported Upstream version 2.72.3
[platform/upstream/glib.git] / glib / ghostutils.c
index c036195..24a7d37 100644 (file)
@@ -6,7 +6,7 @@
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
  * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
+ * version 2.1 of the License, or (at your option) any later version.
  *
  * This library is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General
- * Public License along with this library; if not, write to the
- * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
- * Boston, MA 02111-1307, USA.
+ * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
  */
 
 #include "config.h"
+#include "glibconfig.h"
 
 #include <string.h>
 
+#ifdef G_OS_UNIX
+#include <unistd.h>
+#endif
+
 #include "ghostutils.h"
 
 #include "garray.h"
 #include "gstrfuncs.h"
 #include "glibintl.h"
 
+#ifdef G_PLATFORM_WIN32
+#include <windows.h>
+#endif
+
 
 /**
  * SECTION:ghostutils
@@ -40,9 +47,9 @@
  * converting between Unicode and ASCII-encoded forms of
  * Internationalized Domain Names (IDNs).
  *
- * The <ulink
- * url="http://www.ietf.org/rfc/rfc3490.txt">Internationalized Domain
- * Names for Applications (IDNA)</ulink> standards allow for the use
+ * The
+ * [Internationalized Domain Names for Applications (IDNA)](http://www.ietf.org/rfc/rfc3490.txt)
+ * standards allow for the use
  * of Unicode domain names in applications, while providing
  * backward-compatibility with the old ASCII-only DNS, by defining an
  * ASCII-Compatible Encoding of any given Unicode name, which can be
@@ -121,15 +128,18 @@ punycode_encode (const gchar *input_utf8,
 {
   guint delta, handled_chars, num_basic_chars, bias, j, q, k, t, digit;
   gunichar n, m, *input;
-  glong input_length;
+  glong written_chars;
+  gsize input_length;
   gboolean success = FALSE;
 
   /* Convert from UTF-8 to Unicode code points */
   input = g_utf8_to_ucs4 (input_utf8, input_utf8_length, NULL,
-                         &input_length, NULL);
+                         &written_chars, NULL);
   if (!input)
     return FALSE;
 
+  input_length = (gsize) (written_chars > 0 ? written_chars : 0);
+
   /* Copy basic chars */
   for (j = num_basic_chars = 0; j < input_length; j++)
     {
@@ -303,7 +313,8 @@ idna_is_prohibited (gunichar ch)
 /* RFC 3491 IDN cleanup algorithm. */
 static gchar *
 nameprep (const gchar *hostname,
-          gint         len)
+          gint         len,
+          gboolean    *is_unicode)
 {
   gchar *name, *tmp = NULL, *p;
 
@@ -336,12 +347,15 @@ nameprep (const gchar *hostname,
   /* If there are no UTF8 characters, we're done. */
   if (!contains_non_ascii (name, len))
     {
+      *is_unicode = FALSE;
       if (name == (gchar *)hostname)
         return len == -1 ? g_strdup (hostname) : g_strndup (hostname, len);
       else
         return name;
     }
 
+  *is_unicode = TRUE;
+
   /* Normalize */
   name = g_utf8_normalize (name, len, G_NORMALIZE_NFKC);
   g_free (tmp);
@@ -383,6 +397,65 @@ nameprep (const gchar *hostname,
   return name;
 }
 
+/* RFC 3490, section 3.1 says '.', 0x3002, 0xFF0E, and 0xFF61 count as
+ * label-separating dots. @str must be '\0'-terminated.
+ */
+#define idna_is_dot(str) ( \
+  ((guchar)(str)[0] == '.') ||                                                 \
+  ((guchar)(str)[0] == 0xE3 && (guchar)(str)[1] == 0x80 && (guchar)(str)[2] == 0x82) || \
+  ((guchar)(str)[0] == 0xEF && (guchar)(str)[1] == 0xBC && (guchar)(str)[2] == 0x8E) || \
+  ((guchar)(str)[0] == 0xEF && (guchar)(str)[1] == 0xBD && (guchar)(str)[2] == 0xA1) )
+
+static const gchar *
+idna_end_of_label (const gchar *str)
+{
+  for (; *str; str = g_utf8_next_char (str))
+    {
+      if (idna_is_dot (str))
+        return str;
+    }
+  return str;
+}
+
+static gsize
+get_hostname_max_length_bytes (void)
+{
+#if defined(G_OS_WIN32)
+  wchar_t tmp[MAX_COMPUTERNAME_LENGTH];
+  return sizeof (tmp) / sizeof (tmp[0]);
+#elif defined(_SC_HOST_NAME_MAX)
+  glong max = sysconf (_SC_HOST_NAME_MAX);
+  if (max > 0)
+    return (gsize) max;
+
+#ifdef HOST_NAME_MAX
+  return HOST_NAME_MAX;
+#else
+  return _POSIX_HOST_NAME_MAX;
+#endif /* HOST_NAME_MAX */
+#else
+  /* Fallback to some reasonable value
+   * See https://stackoverflow.com/questions/8724954/what-is-the-maximum-number-of-characters-for-a-host-name-in-unix/28918017#28918017 */
+  return 255;
+#endif
+}
+
+/* Returns %TRUE if `strlen (str) > comparison_length`, but without actually
+ * running `strlen(str)`, as that would take a very long time for long
+ * (untrusted) input strings. */
+static gboolean
+strlen_greater_than (const gchar *str,
+                     gsize        comparison_length)
+{
+  gsize i;
+
+  for (i = 0; str[i] != '\0'; i++)
+    if (i > comparison_length)
+      return TRUE;
+
+  return FALSE;
+}
+
 /**
  * g_hostname_to_ascii:
  * @hostname: a valid UTF-8 or ASCII hostname
@@ -391,8 +464,8 @@ nameprep (const gchar *hostname,
  * string containing no uppercase letters and not ending with a
  * trailing dot.
  *
- * Return value: an ASCII hostname, which must be freed, or %NULL if
- * @hostname is in some way invalid.
+ * Returns: (nullable) (transfer full): an ASCII hostname, which must be freed,
+ *    or %NULL if @hostname is in some way invalid.
  *
  * Since: 2.22
  **/
@@ -403,17 +476,43 @@ g_hostname_to_ascii (const gchar *hostname)
   GString *out;
   gssize llen, oldlen;
   gboolean unicode;
-
-  label = name = nameprep (hostname, -1);
-  if (!name)
+  gsize hostname_max_length_bytes = get_hostname_max_length_bytes ();
+
+  /* Do an initial check on the hostname length, as overlong hostnames take a
+   * long time in the IDN cleanup algorithm in nameprep(). The ultimate
+   * restriction is that the IDN-decoded (i.e. pure ASCII) hostname cannot be
+   * longer than 255 bytes. That’s the least restrictive limit on hostname
+   * length of all the ways hostnames can be interpreted. Typically, the
+   * hostname will be an FQDN, which is limited to 253 bytes long. POSIX
+   * hostnames are limited to `get_hostname_max_length_bytes()` (typically 255
+   * bytes).
+   *
+   * See https://stackoverflow.com/a/28918017/2931197
+   *
+   * It’s possible for a hostname to be %-encoded, in which case its decoded
+   * length will be as much as 3× shorter.
+   *
+   * It’s also possible for a hostname to use overlong UTF-8 encodings, in which
+   * case its decoded length will be as much as 4× shorter.
+   *
+   * Note: This check is not intended as an absolute guarantee that a hostname
+   * is the right length and will be accepted by other systems. It’s intended to
+   * stop wildly-invalid hostnames from taking forever in nameprep().
+   */
+  if (hostname_max_length_bytes <= G_MAXSIZE / 4 &&
+      strlen_greater_than (hostname, 4 * MAX (255, hostname_max_length_bytes)))
     return NULL;
 
+  label = name = nameprep (hostname, -1, &unicode);
+  if (!name || !unicode)
+    return name;
+
   out = g_string_new (NULL);
 
   do
     {
       unicode = FALSE;
-      for (p = label; *p && *p != '.'; p++)
+      for (p = label; *p && !idna_is_dot (p); p++)
        {
          if ((guchar)*p > 0x80)
            unicode = TRUE;
@@ -437,7 +536,9 @@ g_hostname_to_ascii (const gchar *hostname)
        goto fail;
 
       label += llen;
-      if (*label && *++label)
+      if (*label)
+        label = g_utf8_next_char (label);
+      if (*label)
         g_string_append_c (out, '.');
     }
   while (*label);
@@ -463,7 +564,7 @@ g_hostname_to_ascii (const gchar *hostname)
  * segments, and so it is possible for g_hostname_is_non_ascii() and
  * g_hostname_is_ascii_encoded() to both return %TRUE for a name.
  *
- * Return value: %TRUE if @hostname contains any non-ASCII characters
+ * Returns: %TRUE if @hostname contains any non-ASCII characters
  *
  * Since: 2.22
  **/
@@ -570,8 +671,8 @@ punycode_decode (const gchar *input,
  * Of course if @hostname is not an internationalized hostname, then
  * the canonical presentation form will be entirely ASCII.
  *
- * Return value: a UTF-8 hostname, which must be freed, or %NULL if
- * @hostname is in some way invalid.
+ * Returns: (nullable) (transfer full): a UTF-8 hostname, which must be freed,
+ *    or %NULL if @hostname is in some way invalid.
  *
  * Since: 2.22
  **/
@@ -580,12 +681,18 @@ g_hostname_to_unicode (const gchar *hostname)
 {
   GString *out;
   gssize llen;
+  gsize hostname_max_length_bytes = get_hostname_max_length_bytes ();
+
+  /* See the comment at the top of g_hostname_to_ascii(). */
+  if (hostname_max_length_bytes <= G_MAXSIZE / 4 &&
+      strlen_greater_than (hostname, 4 * MAX (255, hostname_max_length_bytes)))
+    return NULL;
 
   out = g_string_new (NULL);
 
   do
     {
-      llen = strcspn (hostname, ".");
+      llen = idna_end_of_label (hostname) - hostname;
       if (!g_ascii_strncasecmp (hostname, IDNA_ACE_PREFIX, IDNA_ACE_PREFIX_LEN))
        {
          hostname += IDNA_ACE_PREFIX_LEN;
@@ -598,7 +705,8 @@ g_hostname_to_unicode (const gchar *hostname)
        }
       else
         {
-          gchar *canonicalized = nameprep (hostname, llen);
+          gboolean unicode;
+          gchar *canonicalized = nameprep (hostname, llen, &unicode);
 
           if (!canonicalized)
             {
@@ -610,7 +718,9 @@ g_hostname_to_unicode (const gchar *hostname)
         }
 
       hostname += llen;
-      if (*hostname && *++hostname)
+      if (*hostname)
+        hostname = g_utf8_next_char (hostname);
+      if (*hostname)
         g_string_append_c (out, '.');
     }
   while (*hostname);
@@ -631,7 +741,7 @@ g_hostname_to_unicode (const gchar *hostname)
  * segments, and so it is possible for g_hostname_is_non_ascii() and
  * g_hostname_is_ascii_encoded() to both return %TRUE for a name.
  *
- * Return value: %TRUE if @hostname contains any ASCII-encoded
+ * Returns: %TRUE if @hostname contains any ASCII-encoded
  * segments.
  *
  * Since: 2.22
@@ -643,8 +753,10 @@ g_hostname_is_ascii_encoded (const gchar *hostname)
     {
       if (!g_ascii_strncasecmp (hostname, IDNA_ACE_PREFIX, IDNA_ACE_PREFIX_LEN))
        return TRUE;
-      hostname = strchr (hostname, '.');
-      if (!hostname++)
+      hostname = idna_end_of_label (hostname);
+      if (*hostname)
+        hostname = g_utf8_next_char (hostname);
+      if (!*hostname)
        return FALSE;
     }
 }
@@ -656,7 +768,9 @@ g_hostname_is_ascii_encoded (const gchar *hostname)
  * Tests if @hostname is the string form of an IPv4 or IPv6 address.
  * (Eg, "192.168.0.1".)
  *
- * Return value: %TRUE if @hostname is an IP address
+ * Since 2.66, IPv6 addresses with a zone-id are accepted (RFC6874).
+ *
+ * Returns: %TRUE if @hostname is an IP address
  *
  * Since: 2.22
  **/
@@ -687,7 +801,7 @@ g_hostname_is_ip_address (const gchar *hostname)
 
       nsegments = 0;
       skipped = FALSE;
-      while (*p && nsegments < 8)
+      while (*p && *p != '%' && nsegments < 8)
         {
           /* Each segment after the first must be preceded by a ':'.
            * (We also handle half of the "string starts with ::" case
@@ -731,7 +845,7 @@ g_hostname_is_ip_address (const gchar *hostname)
           p = end;
         }
 
-      return !*p && (nsegments == 8 || skipped);
+      return (!*p || (p[0] == '%' && p[1])) && (nsegments == 8 || skipped);
     }
 
  parse_ipv4:
@@ -756,7 +870,12 @@ g_hostname_is_ip_address (const gchar *hostname)
       else
         {
           for (end = p; g_ascii_isdigit (*end); end++)
-            octet = 10 * octet + (*end - '0');
+            {
+              octet = 10 * octet + (*end - '0');
+
+              if (octet > 255)
+                break;
+            }
         }
       if (end == p || end > p + 3 || octet > 255)
         return FALSE;