gsturi: Add new API for storing unmodified userinfo / fragment
authorNirbheek Chauhan <nirbheek@centricular.com>
Thu, 30 Jul 2020 14:01:55 +0000 (19:31 +0530)
committerGStreamer Merge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Mon, 3 Aug 2020 16:07:45 +0000 (16:07 +0000)
New API: gst_uri_from_string_escaped()

Identical to gst_uri_from_string() except that the userinfo and
fragment components of the URI will not be unescaped while parsing.

This is needed for correctly parsing usernames or passwords with `:`
in them such as reported at:
https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/831

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/583>

gst/gsturi.c
gst/gsturi.h
tests/check/gst/gsturi.c

index 84f1141..067629b 100644 (file)
@@ -1497,19 +1497,8 @@ gst_uri_new_with_base (GstUri * base, const gchar * scheme,
   return new_uri;
 }
 
-/**
- * gst_uri_from_string:
- * @uri: The URI string to parse.
- *
- * Parses a URI string into a new #GstUri object. Will return NULL if the URI
- * cannot be parsed.
- *
- * Returns: (transfer full) (nullable): A new #GstUri object, or NULL.
- *
- * Since: 1.6
- */
-GstUri *
-gst_uri_from_string (const gchar * uri)
+static GstUri *
+_gst_uri_from_string_internal (const gchar * uri, gboolean unescape)
 {
   const gchar *orig_uri = uri;
   GstUri *uri_obj;
@@ -1545,7 +1534,10 @@ gst_uri_from_string (const gchar * uri)
       /* find end of userinfo */
       eoui = strchr (uri, '@');
       if (eoui != NULL && eoui < eoa) {
-        uri_obj->userinfo = g_uri_unescape_segment (uri, eoui, NULL);
+        if (unescape)
+          uri_obj->userinfo = g_uri_unescape_segment (uri, eoui, NULL);
+        else
+          uri_obj->userinfo = g_strndup (uri, eoui - uri);
         uri = eoui + 1;
       }
       /* find end of host */
@@ -1565,8 +1557,10 @@ gst_uri_from_string (const gchar * uri)
           reoh = eoh = eoa;
       }
       /* don't capture empty host strings */
-      if (eoh != uri)
+      if (eoh != uri) {
+        /* always unescape hostname */
         uri_obj->host = g_uri_unescape_segment (uri, eoh, NULL);
+      }
 
       uri = reoh;
       if (uri < eoa) {
@@ -1620,7 +1614,10 @@ gst_uri_from_string (const gchar * uri)
       }
     }
     if (uri != NULL && uri[0] == '#') {
-      uri_obj->fragment = g_uri_unescape_string (uri + 1, NULL);
+      if (unescape)
+        uri_obj->fragment = g_uri_unescape_string (uri + 1, NULL);
+      else
+        uri_obj->fragment = g_strdup (uri + 1);
     }
   }
 
@@ -1628,6 +1625,51 @@ gst_uri_from_string (const gchar * uri)
 }
 
 /**
+ * gst_uri_from_string:
+ * @uri: The URI string to parse.
+ *
+ * Parses a URI string into a new #GstUri object. Will return NULL if the URI
+ * cannot be parsed.
+ *
+ * Returns: (transfer full) (nullable): A new #GstUri object, or NULL.
+ *
+ * Since: 1.6
+ */
+GstUri *
+gst_uri_from_string (const gchar * uri)
+{
+  return _gst_uri_from_string_internal (uri, TRUE);
+}
+
+/**
+ * gst_uri_from_string_escaped:
+ * @uri: The URI string to parse.
+ *
+ * Parses a URI string into a new #GstUri object. Will return NULL if the URI
+ * cannot be parsed. This is identical to gst_uri_from_string() except that
+ * the userinfo and fragment components of the URI will not be unescaped while
+ * parsing.
+ *
+ * Use this when you need to extract a username and password from the userinfo
+ * such as https://user:password@example.com since either may contain
+ * a URI-escaped ':' character. gst_uri_from_string() will unescape the entire
+ * userinfo component, which will make it impossible to know which ':'
+ * delineates the username and password.
+ *
+ * The same applies to the fragment component of the URI, such as
+ * https://example.com/path#fragment which may contain a URI-escaped '#'.
+ *
+ * Returns: (transfer full) (nullable): A new #GstUri object, or NULL.
+ *
+ * Since: 1.18
+ */
+GstUri *
+gst_uri_from_string_escaped (const gchar * uri)
+{
+  return _gst_uri_from_string_internal (uri, FALSE);
+}
+
+/**
  * gst_uri_from_string_with_base:
  * @base: (transfer none)(nullable): The base URI to join the new URI with.
  * @uri: The URI string to parse.
index 0cbeff3..6f8afed 100644 (file)
@@ -236,6 +236,9 @@ GST_API
 GstUri * gst_uri_from_string           (const gchar * uri) G_GNUC_MALLOC;
 
 GST_API
+GstUri * gst_uri_from_string_escaped   (const gchar * uri) G_GNUC_MALLOC;
+
+GST_API
 GstUri * gst_uri_from_string_with_base (GstUri * base,
                                         const gchar * uri) G_GNUC_MALLOC;
 GST_API
index 8c304c5..3364ee7 100644 (file)
@@ -336,6 +336,21 @@ struct URITest
         {"scheme", "us:er:pa:ss", "hostname", 123, "/path", {{"query", NULL}, \
               {NULL, NULL}}, "frag#ment"}},
 
+#define ESCAPED_URI_TESTS \
+  /* Test cases for gst_uri_from_string_escaped */ \
+  {"scheme://user%20info@hostname", \
+        {"scheme", "user%20info", "hostname", GST_URI_NO_PORT, NULL, {{NULL, \
+                  NULL}}, NULL}}, \
+  {"scheme://userinfo@hostname:123/path?query#frag%23ment", \
+        {"scheme", "userinfo", "hostname", 123, "/path", {{"query", NULL}, \
+              {NULL, NULL}}, "frag%23ment"}}, \
+  {"scheme://us%3Aer:pass@hostname", \
+        {"scheme", "us%3Aer:pass", "hostname", GST_URI_NO_PORT, NULL, {{NULL, \
+                  NULL}}, NULL}}, \
+  {"scheme://us%3Aer:pa%3Ass@hostname:123/path?query#frag%23ment", \
+        {"scheme", "us%3Aer:pa%3Ass", "hostname", 123, "/path", {{"query", NULL}, \
+              {NULL, NULL}}, "frag%23ment"}},
+
 
 static const struct URITest tests[] = {
   COMMON_URI_TESTS UNESCAPED_URI_TESTS
@@ -400,6 +415,63 @@ GST_START_TEST (test_url_parsing)
 
 GST_END_TEST;
 
+
+static const struct URITest escaped_tests[] = {
+  COMMON_URI_TESTS ESCAPED_URI_TESTS
+};
+
+GST_START_TEST (test_url_parsing_escaped)
+{
+  GstUri *uri;
+  GList *list;
+  gchar *tmp_str;
+  guint i, j;
+
+  for (i = 0; i < G_N_ELEMENTS (escaped_tests); i++) {
+    GST_DEBUG ("Testing URI '%s'", escaped_tests[i].str);
+
+    uri = gst_uri_from_string_escaped (escaped_tests[i].str);
+    fail_unless (uri != NULL);
+    fail_unless_equals_string (gst_uri_get_scheme (uri),
+        escaped_tests[i].uri.scheme);
+    fail_unless_equals_string (gst_uri_get_userinfo (uri),
+        escaped_tests[i].uri.userinfo);
+    fail_unless_equals_string (gst_uri_get_host (uri),
+        escaped_tests[i].uri.host);
+    fail_unless_equals_int (gst_uri_get_port (uri), escaped_tests[i].uri.port);
+    tmp_str = gst_uri_get_path (uri);
+    fail_unless_equals_string (tmp_str, escaped_tests[i].uri.path);
+    g_free (tmp_str);
+
+    for (j = 0; j < 10; j++) {
+      if (!escaped_tests[i].uri.query[j].key)
+        break;
+
+      if (escaped_tests[i].uri.query[j].value) {
+        fail_unless_equals_string (gst_uri_get_query_value (uri,
+                escaped_tests[i].uri.query[j].key),
+            escaped_tests[i].uri.query[j].value);
+      } else {
+        fail_unless (gst_uri_query_has_key (uri,
+                escaped_tests[i].uri.query[j].key));
+      }
+    }
+    list = gst_uri_get_query_keys (uri);
+    fail_unless_equals_int (j, g_list_length (list));
+    g_list_free (list);
+    gst_uri_unref (uri);
+  }
+
+  for (i = 0; i < G_N_ELEMENTS (unparsable_uri_tests); i++) {
+    GST_DEBUG ("Testing unparsable URI '%s'", unparsable_uri_tests[i]);
+
+    uri = gst_uri_from_string (unparsable_uri_tests[i]);
+    fail_unless (uri == NULL);
+  }
+}
+
+GST_END_TEST;
+
 static const struct URITest url_presenting_tests[] = {
   /* check all URI elements present */
   {.uri = {"scheme", "user:pass", "host", 1234, "/path/to/dir",
@@ -1141,6 +1213,7 @@ gst_uri_suite (void)
   tcase_add_test (tc_chain, test_win32_uri);
 #endif
   tcase_add_test (tc_chain, test_url_parsing);
+  tcase_add_test (tc_chain, test_url_parsing_escaped);
   tcase_add_test (tc_chain, test_url_presenting);
   tcase_add_test (tc_chain, test_url_normalization);
   tcase_add_test (tc_chain, test_url_joining);