/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
* Copyright © 1995-1998 Free Software Foundation, Inc.
* Copyright © 2014-2019 Red Hat, Inc
*
* This program 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.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
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* 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, see .
*
* Authors:
* Alexander Larsson
*/
#include "config.h"
#include
#include "flatpak-uri-private.h"
#if !GLIB_CHECK_VERSION (2, 66, 0)
struct _GUri {
gchar *scheme;
gchar *userinfo;
gchar *host;
gint port;
gchar *path;
gchar *query;
gchar *fragment;
gchar *user;
gchar *password;
gchar *auth_params;
GUriFlags flags;
int ref_count;
};
GUri *
flatpak_g_uri_ref (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
g_atomic_int_inc (&uri->ref_count);
return uri;
}
void
flatpak_g_uri_unref (GUri *uri)
{
g_return_if_fail (uri != NULL);
if (g_atomic_int_dec_and_test (&uri->ref_count))
{
g_free (uri->scheme);
g_free (uri->userinfo);
g_free (uri->host);
g_free (uri->path);
g_free (uri->query);
g_free (uri->fragment);
g_free (uri->user);
g_free (uri->password);
g_free (uri->auth_params);
g_free (uri);
}
}
static gboolean
flatpak_g_uri_char_is_unreserved (gchar ch)
{
if (g_ascii_isalnum (ch))
return TRUE;
return ch == '-' || ch == '.' || ch == '_' || ch == '~';
}
#define XDIGIT(c) ((c) <= '9' ? (c) - '0' : ((c) & 0x4F) - 'A' + 10)
#define HEXCHAR(s) ((XDIGIT (s[1]) << 4) + XDIGIT (s[2]))
static gssize
uri_decoder (gchar **out,
const gchar *illegal_chars,
const gchar *start,
gsize length,
gboolean just_normalize,
gboolean www_form,
GUriFlags flags,
GError **error)
{
gchar c;
GString *decoded;
const gchar *invalid, *s, *end;
gssize len;
if (!(flags & G_URI_FLAGS_ENCODED))
just_normalize = FALSE;
decoded = g_string_sized_new (length + 1);
for (s = start, end = s + length; s < end; s++)
{
if (*s == '%')
{
if (s + 2 >= end ||
!g_ascii_isxdigit (s[1]) ||
!g_ascii_isxdigit (s[2]))
{
/* % followed by non-hex or the end of the string; this is an error */
if (!(flags & G_URI_FLAGS_PARSE_RELAXED))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
/* xgettext: no-c-format */
_("Invalid %-encoding in URI"));
g_string_free (decoded, TRUE);
return -1;
}
/* In non-strict mode, just let it through; we *don't*
* fix it to "%25", since that might change the way that
* the URI's owner would interpret it.
*/
g_string_append_c (decoded, *s);
continue;
}
c = HEXCHAR (s);
if (illegal_chars && strchr (illegal_chars, c))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Illegal character in URI"));
g_string_free (decoded, TRUE);
return -1;
}
if (just_normalize && !flatpak_g_uri_char_is_unreserved (c))
{
/* Leave the % sequence there but normalize it. */
g_string_append_c (decoded, *s);
g_string_append_c (decoded, g_ascii_toupper (s[1]));
g_string_append_c (decoded, g_ascii_toupper (s[2]));
s += 2;
}
else
{
g_string_append_c (decoded, c);
s += 2;
}
}
else if (www_form && *s == '+')
g_string_append_c (decoded, ' ');
/* Normalize any illegal characters. */
else if (just_normalize && (!g_ascii_isgraph (*s)))
g_string_append_printf (decoded, "%%%02X", (guchar)*s);
else
g_string_append_c (decoded, *s);
}
len = decoded->len;
g_assert (len >= 0);
if (!(flags & G_URI_FLAGS_ENCODED) &&
!g_utf8_validate (decoded->str, len, &invalid))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Non-UTF-8 characters in URI"));
g_string_free (decoded, TRUE);
return -1;
}
if (out)
*out = g_string_free (decoded, FALSE);
else
g_string_free (decoded, TRUE);
return len;
}
static gboolean
uri_decode (gchar **out,
const gchar *illegal_chars,
const gchar *start,
gsize length,
gboolean www_form,
GUriFlags flags,
GError **error)
{
return uri_decoder (out, illegal_chars, start, length, FALSE, www_form, flags,
error) != -1;
}
static gboolean
uri_normalize (gchar **out,
const gchar *start,
gsize length,
GUriFlags flags,
GError **error)
{
return uri_decoder (out, NULL, start, length, TRUE, FALSE, flags,
error) != -1;
}
static gboolean
parse_ip_literal (const gchar *start,
gsize length,
GUriFlags flags,
gchar **out,
GError **error)
{
gchar *pct, *zone_id = NULL;
gchar *addr = NULL;
gsize addr_length = 0;
gsize zone_id_length = 0;
gchar *decoded_zone_id = NULL;
if (start[length - 1] != ']')
goto bad_ipv6_literal;
/* Drop the square brackets */
addr = g_strndup (start + 1, length - 2);
addr_length = length - 2;
/* If there's an IPv6 scope ID, split out the zone. */
pct = strchr (addr, '%');
if (pct != NULL)
{
*pct = '\0';
if (addr_length - (pct - addr) >= 4 &&
*(pct + 1) == '2' && *(pct + 2) == '5')
{
zone_id = pct + 3;
zone_id_length = addr_length - (zone_id - addr);
}
else if (flags & G_URI_FLAGS_PARSE_RELAXED &&
addr_length - (pct - addr) >= 2)
{
zone_id = pct + 1;
zone_id_length = addr_length - (zone_id - addr);
}
else
goto bad_ipv6_literal;
g_assert (zone_id_length >= 1);
}
/* addr must be an IPv6 address */
if (!g_hostname_is_ip_address (addr) || !strchr (addr, ':'))
goto bad_ipv6_literal;
/* Zone ID must be valid. It can contain %-encoded characters. */
if (zone_id != NULL &&
!uri_decode (&decoded_zone_id, NULL, zone_id, zone_id_length, FALSE,
flags, NULL))
goto bad_ipv6_literal;
/* Success */
if (out != NULL && decoded_zone_id != NULL)
*out = g_strconcat (addr, "%", decoded_zone_id, NULL);
else if (out != NULL)
*out = g_steal_pointer (&addr);
g_free (addr);
g_free (decoded_zone_id);
return TRUE;
bad_ipv6_literal:
g_free (addr);
g_free (decoded_zone_id);
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Invalid IPv6 address ‘%.*s’ in URI"),
(gint)length, start);
return FALSE;
}
static gboolean
parse_host (const gchar *start,
gsize length,
GUriFlags flags,
gchar **out,
GError **error)
{
gchar *decoded = NULL, *host;
gchar *addr = NULL;
if (*start == '[')
{
if (!parse_ip_literal (start, length, flags, &host, error))
return FALSE;
goto ok;
}
if (g_ascii_isdigit (*start))
{
addr = g_strndup (start, length);
if (g_hostname_is_ip_address (addr))
{
host = addr;
goto ok;
}
g_free (addr);
}
if (flags & G_URI_FLAGS_NON_DNS)
{
if (!uri_normalize (&decoded, start, length, flags,
error))
return FALSE;
host = g_steal_pointer (&decoded);
goto ok;
}
flags &= ~G_URI_FLAGS_ENCODED;
if (!uri_decode (&decoded, NULL, start, length, FALSE, flags,
error))
return FALSE;
/* You're not allowed to %-encode an IP address, so if it wasn't
* one before, it better not be one now.
*/
if (g_hostname_is_ip_address (decoded))
{
g_free (decoded);
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Illegal encoded IP address ‘%.*s’ in URI"),
(gint)length, start);
return FALSE;
}
if (g_hostname_is_non_ascii (decoded))
{
host = g_hostname_to_ascii (decoded);
if (host == NULL)
{
g_free (decoded);
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Illegal internationalized hostname ‘%.*s’ in URI"),
(gint) length, start);
return FALSE;
}
}
else
{
host = g_steal_pointer (&decoded);
}
ok:
if (out)
*out = g_steal_pointer (&host);
g_free (host);
g_free (decoded);
return TRUE;
}
static gboolean
parse_port (const gchar *start,
gsize length,
gint *out,
GError **error)
{
gchar *end;
gulong parsed_port;
/* strtoul() allows leading + or -, so we have to check this first. */
if (!g_ascii_isdigit (*start))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Could not parse port ‘%.*s’ in URI"),
(gint)length, start);
return FALSE;
}
/* We know that *(start + length) is either '\0' or a non-numeric
* character, so strtoul() won't scan beyond it.
*/
parsed_port = strtoul (start, &end, 10);
if (end != start + length)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Could not parse port ‘%.*s’ in URI"),
(gint)length, start);
return FALSE;
}
else if (parsed_port > 65535)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Port ‘%.*s’ in URI is out of range"),
(gint)length, start);
return FALSE;
}
if (out)
*out = parsed_port;
return TRUE;
}
static gboolean
parse_userinfo (const gchar *start,
gsize length,
GUriFlags flags,
gchar **user,
gchar **password,
gchar **auth_params,
GError **error)
{
const gchar *user_end = NULL, *password_end = NULL, *auth_params_end;
auth_params_end = start + length;
if (flags & G_URI_FLAGS_HAS_AUTH_PARAMS)
password_end = memchr (start, ';', auth_params_end - start);
if (!password_end)
password_end = auth_params_end;
if (flags & G_URI_FLAGS_HAS_PASSWORD)
user_end = memchr (start, ':', password_end - start);
if (!user_end)
user_end = password_end;
if (!uri_normalize (user, start, user_end - start, flags,
error))
return FALSE;
if (*user_end == ':')
{
start = user_end + 1;
if (!uri_normalize (password, start, password_end - start, flags,
error))
{
if (user)
g_clear_pointer (user, g_free);
return FALSE;
}
}
else if (password)
*password = NULL;
if (*password_end == ';')
{
start = password_end + 1;
if (!uri_normalize (auth_params, start, auth_params_end - start, flags,
error))
{
if (user)
g_clear_pointer (user, g_free);
if (password)
g_clear_pointer (password, g_free);
return FALSE;
}
}
else if (auth_params)
*auth_params = NULL;
return TRUE;
}
static gchar *
uri_cleanup (const gchar *uri_string)
{
GString *copy;
const gchar *end;
/* Skip leading whitespace */
while (g_ascii_isspace (*uri_string))
uri_string++;
/* Ignore trailing whitespace */
end = uri_string + strlen (uri_string);
while (end > uri_string && g_ascii_isspace (*(end - 1)))
end--;
/* Copy the rest, encoding unencoded spaces and stripping other whitespace */
copy = g_string_sized_new (end - uri_string);
while (uri_string < end)
{
if (*uri_string == ' ')
g_string_append (copy, "%20");
else if (g_ascii_isspace (*uri_string))
;
else
g_string_append_c (copy, *uri_string);
uri_string++;
}
return g_string_free (copy, FALSE);
}
static gboolean
should_normalize_empty_path (const char *scheme)
{
const char * const schemes[] = { "https", "http", "wss", "ws" };
gsize i;
for (i = 0; i < G_N_ELEMENTS (schemes); ++i)
{
if (!strcmp (schemes[i], scheme))
return TRUE;
}
return FALSE;
}
static int
normalize_port (const char *scheme,
int port)
{
const char *default_schemes[3] = { NULL };
int i;
switch (port)
{
case 21:
default_schemes[0] = "ftp";
break;
case 80:
default_schemes[0] = "http";
default_schemes[1] = "ws";
break;
case 443:
default_schemes[0] = "https";
default_schemes[1] = "wss";
break;
default:
break;
}
for (i = 0; default_schemes[i]; ++i)
{
if (!strcmp (scheme, default_schemes[i]))
return -1;
}
return port;
}
static int
default_scheme_port (const char *scheme)
{
if (strcmp (scheme, "http") == 0 || strcmp (scheme, "ws") == 0)
return 80;
if (strcmp (scheme, "https") == 0 || strcmp (scheme, "wss") == 0)
return 443;
if (strcmp (scheme, "ftp") == 0)
return 21;
return -1;
}
static gboolean
flatpak_g_uri_split_internal (const gchar *uri_string,
GUriFlags flags,
gchar **scheme,
gchar **userinfo,
gchar **user,
gchar **password,
gchar **auth_params,
gchar **host,
gint *port,
gchar **path,
gchar **query,
gchar **fragment,
GError **error)
{
const gchar *end, *colon, *at, *path_start, *semi, *question;
const gchar *p, *bracket, *hostend;
gchar *cleaned_uri_string = NULL;
gchar *normalized_scheme = NULL;
if (scheme)
*scheme = NULL;
if (userinfo)
*userinfo = NULL;
if (user)
*user = NULL;
if (password)
*password = NULL;
if (auth_params)
*auth_params = NULL;
if (host)
*host = NULL;
if (port)
*port = -1;
if (path)
*path = NULL;
if (query)
*query = NULL;
if (fragment)
*fragment = NULL;
if ((flags & G_URI_FLAGS_PARSE_RELAXED) && strpbrk (uri_string, " \t\n\r"))
{
cleaned_uri_string = uri_cleanup (uri_string);
uri_string = cleaned_uri_string;
}
/* Find scheme */
p = uri_string;
while (*p && (g_ascii_isalpha (*p) ||
(p > uri_string && (g_ascii_isdigit (*p) ||
*p == '.' || *p == '+' || *p == '-'))))
p++;
if (p > uri_string && *p == ':')
{
normalized_scheme = g_ascii_strdown (uri_string, p - uri_string);
if (scheme)
*scheme = g_steal_pointer (&normalized_scheme);
p++;
}
else
{
if (scheme)
*scheme = NULL;
p = uri_string;
}
/* Check for authority */
if (strncmp (p, "//", 2) == 0)
{
p += 2;
path_start = p + strcspn (p, "/?#");
at = memchr (p, '@', path_start - p);
if (at)
{
if (flags & G_URI_FLAGS_PARSE_RELAXED)
{
gchar *next_at;
/* Any "@"s in the userinfo must be %-encoded, but
* people get this wrong sometimes. Since "@"s in the
* hostname are unlikely (and also wrong anyway), assume
* that if there are extra "@"s, they belong in the
* userinfo.
*/
do
{
next_at = memchr (at + 1, '@', path_start - (at + 1));
if (next_at)
at = next_at;
}
while (next_at);
}
if (user || password || auth_params ||
(flags & (G_URI_FLAGS_HAS_PASSWORD|G_URI_FLAGS_HAS_AUTH_PARAMS)))
{
if (!parse_userinfo (p, at - p, flags,
user, password, auth_params,
error))
goto fail;
}
if (!uri_normalize (userinfo, p, at - p, flags,
error))
goto fail;
p = at + 1;
}
if (flags & G_URI_FLAGS_PARSE_RELAXED)
{
semi = strchr (p, ';');
if (semi && semi < path_start)
{
/* Technically, semicolons are allowed in the "host"
* production, but no one ever does this, and some
* schemes mistakenly use semicolon as a delimiter
* marking the start of the path. We have to check this
* after checking for userinfo though, because a
* semicolon before the "@" must be part of the
* userinfo.
*/
path_start = semi;
}
}
/* Find host and port. The host may be a bracket-delimited IPv6
* address, in which case the colon delimiting the port must come
* (immediately) after the close bracket.
*/
if (*p == '[')
{
bracket = memchr (p, ']', path_start - p);
if (bracket && *(bracket + 1) == ':')
colon = bracket + 1;
else
colon = NULL;
}
else
colon = memchr (p, ':', path_start - p);
hostend = colon ? colon : path_start;
if (!parse_host (p, hostend - p, flags, host, error))
goto fail;
if (colon && colon != path_start - 1)
{
p = colon + 1;
if (!parse_port (p, path_start - p, port, error))
goto fail;
}
p = path_start;
}
/* Find fragment. */
end = p + strcspn (p, "#");
if (*end == '#')
{
if (!uri_normalize (fragment, end + 1, strlen (end + 1),
flags | (flags & G_URI_FLAGS_ENCODED_FRAGMENT ? G_URI_FLAGS_ENCODED : 0),
error))
goto fail;
}
/* Find query */
question = memchr (p, '?', end - p);
if (question)
{
if (!uri_normalize (query, question + 1, end - (question + 1),
flags | (flags & G_URI_FLAGS_ENCODED_QUERY ? G_URI_FLAGS_ENCODED : 0),
error))
goto fail;
end = question;
}
if (!uri_normalize (path, p, end - p,
flags | (flags & G_URI_FLAGS_ENCODED_PATH ? G_URI_FLAGS_ENCODED : 0),
error))
goto fail;
/* Scheme-based normalization */
if (flags & G_URI_FLAGS_SCHEME_NORMALIZE && ((scheme && *scheme) || normalized_scheme))
{
const char *scheme_str = scheme && *scheme ? *scheme : normalized_scheme;
if (should_normalize_empty_path (scheme_str) && path && !**path)
{
g_free (*path);
*path = g_strdup ("/");
}
if (port && *port == -1)
*port = default_scheme_port (scheme_str);
}
g_free (normalized_scheme);
g_free (cleaned_uri_string);
return TRUE;
fail:
if (scheme)
g_clear_pointer (scheme, g_free);
if (userinfo)
g_clear_pointer (userinfo, g_free);
if (host)
g_clear_pointer (host, g_free);
if (port)
*port = -1;
if (path)
g_clear_pointer (path, g_free);
if (query)
g_clear_pointer (query, g_free);
if (fragment)
g_clear_pointer (fragment, g_free);
g_free (normalized_scheme);
g_free (cleaned_uri_string);
return FALSE;
}
/* Implements the "Remove Dot Segments" algorithm from section 5.2.4 of
* RFC 3986.
*
* See https://tools.ietf.org/html/rfc3986#section-5.2.4
*/
static void
remove_dot_segments (gchar *path)
{
/* The output can be written to the same buffer that the input
* is read from, as the output pointer is only ever increased
* when the input pointer is increased as well, and the input
* pointer is never decreased. */
gchar *input = path;
gchar *output = path;
if (!*path)
return;
while (*input)
{
/* A. If the input buffer begins with a prefix of "../" or "./",
* then remove that prefix from the input buffer; otherwise,
*/
if (strncmp (input, "../", 3) == 0)
input += 3;
else if (strncmp (input, "./", 2) == 0)
input += 2;
/* B. if the input buffer begins with a prefix of "/./" or "/.",
* where "." is a complete path segment, then replace that
* prefix with "/" in the input buffer; otherwise,
*/
else if (strncmp (input, "/./", 3) == 0)
input += 2;
else if (strcmp (input, "/.") == 0)
input[1] = '\0';
/* C. if the input buffer begins with a prefix of "/../" or "/..",
* where ".." is a complete path segment, then replace that
* prefix with "/" in the input buffer and remove the last
* segment and its preceding "/" (if any) from the output
* buffer; otherwise,
*/
else if (strncmp (input, "/../", 4) == 0)
{
input += 3;
if (output > path)
{
do
{
output--;
}
while (*output != '/' && output > path);
}
}
else if (strcmp (input, "/..") == 0)
{
input[1] = '\0';
if (output > path)
{
do
{
output--;
}
while (*output != '/' && output > path);
}
}
/* D. if the input buffer consists only of "." or "..", then remove
* that from the input buffer; otherwise,
*/
else if (strcmp (input, "..") == 0 || strcmp (input, ".") == 0)
input[0] = '\0';
/* E. move the first path segment in the input buffer to the end of
* the output buffer, including the initial "/" character (if
* any) and any subsequent characters up to, but not including,
* the next "/" character or the end of the input buffer.
*/
else
{
*output++ = *input++;
while (*input && *input != '/')
*output++ = *input++;
}
}
*output = '\0';
}
GUri *
flatpak_g_uri_parse (const gchar *uri_string,
GUriFlags flags,
GError **error)
{
g_return_val_if_fail (uri_string != NULL, NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
return flatpak_g_uri_parse_relative (NULL, uri_string, flags, error);
}
GUri *
flatpak_g_uri_parse_relative (GUri *base_uri,
const gchar *uri_ref,
GUriFlags flags,
GError **error)
{
GUri *uri = NULL;
g_return_val_if_fail (uri_ref != NULL, NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
g_return_val_if_fail (base_uri == NULL || base_uri->scheme != NULL, NULL);
/* Use GUri struct to construct the return value: there is no guarantee it is
* actually correct within the function body. */
uri = g_new0 (GUri, 1);
uri->ref_count = 1;
uri->flags = flags;
if (!flatpak_g_uri_split_internal (uri_ref, flags,
&uri->scheme, &uri->userinfo,
&uri->user, &uri->password, &uri->auth_params,
&uri->host, &uri->port,
&uri->path, &uri->query, &uri->fragment,
error))
{
flatpak_g_uri_unref (uri);
return NULL;
}
if (!uri->scheme && !base_uri)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("URI is not absolute, and no base URI was provided"));
flatpak_g_uri_unref (uri);
return NULL;
}
if (base_uri)
{
/* This is section 5.2.2 of RFC 3986, except that we're doing
* it in place in @uri rather than copying from R to T.
*
* See https://tools.ietf.org/html/rfc3986#section-5.2.2
*/
if (uri->scheme)
remove_dot_segments (uri->path);
else
{
uri->scheme = g_strdup (base_uri->scheme);
if (uri->host)
remove_dot_segments (uri->path);
else
{
if (!*uri->path)
{
g_free (uri->path);
uri->path = g_strdup (base_uri->path);
if (!uri->query)
uri->query = g_strdup (base_uri->query);
}
else
{
if (*uri->path == '/')
remove_dot_segments (uri->path);
else
{
gchar *newpath, *last;
last = strrchr (base_uri->path, '/');
if (last)
{
newpath = g_strdup_printf ("%.*s/%s",
(gint)(last - base_uri->path),
base_uri->path,
uri->path);
}
else
newpath = g_strdup_printf ("/%s", uri->path);
g_free (uri->path);
uri->path = g_steal_pointer (&newpath);
remove_dot_segments (uri->path);
}
}
uri->userinfo = g_strdup (base_uri->userinfo);
uri->user = g_strdup (base_uri->user);
uri->password = g_strdup (base_uri->password);
uri->auth_params = g_strdup (base_uri->auth_params);
uri->host = g_strdup (base_uri->host);
uri->port = base_uri->port;
}
}
/* Scheme normalization couldn't have been done earlier
* as the relative URI may not have had a scheme */
if (flags & G_URI_FLAGS_SCHEME_NORMALIZE)
{
if (should_normalize_empty_path (uri->scheme) && !*uri->path)
{
g_free (uri->path);
uri->path = g_strdup ("/");
}
uri->port = normalize_port (uri->scheme, uri->port);
}
}
else
{
remove_dot_segments (uri->path);
}
return g_steal_pointer (&uri);
}
/* userinfo as a whole can contain sub-delims + ":", but split-out
* user can't contain ":" or ";", and split-out password can't contain
* ";".
*/
#define USERINFO_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_USERINFO
#define USER_ALLOWED_CHARS "!$&'()*+,="
#define PASSWORD_ALLOWED_CHARS "!$&'()*+,=:"
#define AUTH_PARAMS_ALLOWED_CHARS USERINFO_ALLOWED_CHARS
#define IP_ADDR_ALLOWED_CHARS ":"
#define HOST_ALLOWED_CHARS G_URI_RESERVED_CHARS_SUBCOMPONENT_DELIMITERS
#define PATH_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH
#define QUERY_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH "?"
#define FRAGMENT_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH "?"
static gchar *
flatpak_g_uri_join_internal (GUriFlags flags,
const gchar *scheme,
gboolean userinfo,
const gchar *user,
const gchar *password,
const gchar *auth_params,
const gchar *host,
gint port,
const gchar *path,
const gchar *query,
const gchar *fragment)
{
gboolean encoded = (flags & G_URI_FLAGS_ENCODED);
GString *str;
char *normalized_scheme = NULL;
/* Restrictions on path prefixes. See:
* https://tools.ietf.org/html/rfc3986#section-3
*/
g_return_val_if_fail (path != NULL, NULL);
g_return_val_if_fail (host == NULL || (path[0] == '\0' || path[0] == '/'), NULL);
g_return_val_if_fail (host != NULL || (path[0] != '/' || path[1] != '/'), NULL);
str = g_string_new (scheme);
if (scheme)
g_string_append_c (str, ':');
if (flags & G_URI_FLAGS_SCHEME_NORMALIZE && scheme && ((host && port != -1) || path[0] == '\0'))
normalized_scheme = g_ascii_strdown (scheme, -1);
if (host)
{
g_string_append (str, "//");
if (user)
{
if (encoded)
g_string_append (str, user);
else
{
if (userinfo)
g_string_append_uri_escaped (str, user, USERINFO_ALLOWED_CHARS, TRUE);
else
/* Encode ':' and ';' regardless of whether we have a
* password or auth params, since it may be parsed later
* under the assumption that it does.
*/
g_string_append_uri_escaped (str, user, USER_ALLOWED_CHARS, TRUE);
}
if (password)
{
g_string_append_c (str, ':');
if (encoded)
g_string_append (str, password);
else
g_string_append_uri_escaped (str, password,
PASSWORD_ALLOWED_CHARS, TRUE);
}
if (auth_params)
{
g_string_append_c (str, ';');
if (encoded)
g_string_append (str, auth_params);
else
g_string_append_uri_escaped (str, auth_params,
AUTH_PARAMS_ALLOWED_CHARS, TRUE);
}
g_string_append_c (str, '@');
}
if (strchr (host, ':') && g_hostname_is_ip_address (host))
{
g_string_append_c (str, '[');
if (encoded)
g_string_append (str, host);
else
g_string_append_uri_escaped (str, host, IP_ADDR_ALLOWED_CHARS, TRUE);
g_string_append_c (str, ']');
}
else
{
if (encoded)
g_string_append (str, host);
else
g_string_append_uri_escaped (str, host, HOST_ALLOWED_CHARS, TRUE);
}
if (port != -1 && (!normalized_scheme || normalize_port (normalized_scheme, port) != -1))
g_string_append_printf (str, ":%d", port);
}
if (path[0] == '\0' && normalized_scheme && should_normalize_empty_path (normalized_scheme))
g_string_append (str, "/");
else if (encoded || flags & G_URI_FLAGS_ENCODED_PATH)
g_string_append (str, path);
else
g_string_append_uri_escaped (str, path, PATH_ALLOWED_CHARS, TRUE);
g_free (normalized_scheme);
if (query)
{
g_string_append_c (str, '?');
if (encoded || flags & G_URI_FLAGS_ENCODED_QUERY)
g_string_append (str, query);
else
g_string_append_uri_escaped (str, query, QUERY_ALLOWED_CHARS, TRUE);
}
if (fragment)
{
g_string_append_c (str, '#');
if (encoded || flags & G_URI_FLAGS_ENCODED_FRAGMENT)
g_string_append (str, fragment);
else
g_string_append_uri_escaped (str, fragment, FRAGMENT_ALLOWED_CHARS, TRUE);
}
return g_string_free (str, FALSE);
}
static gchar *
flatpak_g_uri_join (GUriFlags flags,
const gchar *scheme,
const gchar *userinfo,
const gchar *host,
gint port,
const gchar *path,
const gchar *query,
const gchar *fragment)
{
g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
g_return_val_if_fail (path != NULL, NULL);
return flatpak_g_uri_join_internal (flags,
scheme,
TRUE, userinfo, NULL, NULL,
host,
port,
path,
query,
fragment);
}
static gchar *
flatpak_g_uri_join_with_user (GUriFlags flags,
const gchar *scheme,
const gchar *user,
const gchar *password,
const gchar *auth_params,
const gchar *host,
gint port,
const gchar *path,
const gchar *query,
const gchar *fragment)
{
g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
g_return_val_if_fail (path != NULL, NULL);
return flatpak_g_uri_join_internal (flags,
scheme,
FALSE, user, password, auth_params,
host,
port,
path,
query,
fragment);
}
GUri *
flatpak_g_uri_build (GUriFlags flags,
const gchar *scheme,
const gchar *userinfo,
const gchar *host,
gint port,
const gchar *path,
const gchar *query,
const gchar *fragment)
{
GUri *uri;
g_return_val_if_fail (scheme != NULL, NULL);
g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
g_return_val_if_fail (path != NULL, NULL);
uri = g_new0 (GUri, 1);
uri->ref_count = 1;
uri->flags = flags;
uri->scheme = g_ascii_strdown (scheme, -1);
uri->userinfo = g_strdup (userinfo);
uri->host = g_strdup (host);
uri->port = port;
uri->path = g_strdup (path);
uri->query = g_strdup (query);
uri->fragment = g_strdup (fragment);
return g_steal_pointer (&uri);
}
gchar *
flatpak_g_uri_to_string_partial (GUri *uri,
GUriHideFlags flags)
{
gboolean hide_user = (flags & G_URI_HIDE_USERINFO);
gboolean hide_password = (flags & (G_URI_HIDE_USERINFO | G_URI_HIDE_PASSWORD));
gboolean hide_auth_params = (flags & (G_URI_HIDE_USERINFO | G_URI_HIDE_AUTH_PARAMS));
gboolean hide_query = (flags & G_URI_HIDE_QUERY);
gboolean hide_fragment = (flags & G_URI_HIDE_FRAGMENT);
g_return_val_if_fail (uri != NULL, NULL);
if (uri->flags & (G_URI_FLAGS_HAS_PASSWORD | G_URI_FLAGS_HAS_AUTH_PARAMS))
{
return flatpak_g_uri_join_with_user (uri->flags,
uri->scheme,
hide_user ? NULL : uri->user,
hide_password ? NULL : uri->password,
hide_auth_params ? NULL : uri->auth_params,
uri->host,
uri->port,
uri->path,
hide_query ? NULL : uri->query,
hide_fragment ? NULL : uri->fragment);
}
return flatpak_g_uri_join (uri->flags,
uri->scheme,
hide_user ? NULL : uri->userinfo,
uri->host,
uri->port,
uri->path,
hide_query ? NULL : uri->query,
hide_fragment ? NULL : uri->fragment);
}
const gchar *
flatpak_g_uri_get_scheme (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->scheme;
}
const gchar *
flatpak_g_uri_get_userinfo (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->userinfo;
}
const gchar *
flatpak_g_uri_get_user (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->user;
}
const gchar *
flatpak_g_uri_get_password (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->password;
}
const gchar *
flatpak_g_uri_get_auth_params (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->auth_params;
}
const gchar *
flatpak_g_uri_get_host (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->host;
}
gint
flatpak_g_uri_get_port (GUri *uri)
{
g_return_val_if_fail (uri != NULL, -1);
if (uri->port == -1 && uri->flags & G_URI_FLAGS_SCHEME_NORMALIZE)
return default_scheme_port (uri->scheme);
return uri->port;
}
const gchar *
flatpak_g_uri_get_path (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->path;
}
const gchar *
flatpak_g_uri_get_query (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->query;
}
const gchar *
flatpak_g_uri_get_fragment (GUri *uri)
{
g_return_val_if_fail (uri != NULL, NULL);
return uri->fragment;
}
GUriFlags
flatpak_g_uri_get_flags (GUri *uri)
{
g_return_val_if_fail (uri != NULL, G_URI_FLAGS_NONE);
return uri->flags;
}
#endif /* GLIB_CHECK_VERSION (2, 66, 0) */
static void
append_form_encoded (GString *str, const char *in)
{
const unsigned char *s = (const unsigned char *)in;
while (*s)
{
if (*s == ' ')
{
g_string_append_c (str, '+');
s++;
}
else if (!g_ascii_isalnum (*s) && (*s != '-') && (*s != '_')
&& (*s != '.'))
g_string_append_printf (str, "%%%02X", (int)*s++);
else
g_string_append_c (str, *s++);
}
}
void
flatpak_uri_encode_query_arg (GString *str,
const char *key,
const char *value)
{
if (str->len)
g_string_append_c (str, '&');
append_form_encoded (str, key);
g_string_append_c (str, '=');
append_form_encoded (str, value);
}
/* This is a simplified copy of soup_header_parse_param_list() to avoid a soup dependency */
static const char *
skip_lws (const char *s)
{
while (g_ascii_isspace (*s))
s++;
return s;
}
static const char *
unskip_lws (const char *s, const char *start)
{
while (s > start && g_ascii_isspace (*(s - 1)))
s--;
return s;
}
static const char *
skip_delims (const char *s, char delim)
{
/* The grammar allows for multiple delimiters */
while (g_ascii_isspace (*s) || *s == delim)
s++;
return s;
}
static const char *
skip_item (const char *s, char delim)
{
gboolean quoted = FALSE;
const char *start = s;
/* A list item ends at the last non-whitespace character
* before a delimiter which is not inside a quoted-string. Or
* at the end of the string.
*/
while (*s)
{
if (*s == '"')
quoted = !quoted;
else if (quoted)
{
if (*s == '\\' && *(s + 1))
s++;
}
else
{
if (*s == delim)
break;
}
s++;
}
return unskip_lws (s, start);
}
static GSList *
parse_list (const char *header, char delim)
{
GSList *list = NULL;
const char *end;
header = skip_delims (header, delim);
while (*header)
{
end = skip_item (header, delim);
list = g_slist_prepend (list, g_strndup (header, end - header));
header = skip_delims (end, delim);
}
return g_slist_reverse (list);
}
static void
decode_quoted_string (char *quoted_string)
{
char *src, *dst;
src = quoted_string + 1;
dst = quoted_string;
while (*src && *src != '"')
{
if (*src == '\\' && *(src + 1))
src++;
*dst++ = *src++;
}
*dst = '\0';
}
GHashTable *
flatpak_parse_http_header_param_list (const char *header)
{
GHashTable *params;
GSList *list, *iter;
char *eq, *name_end, *value;
params = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free, g_free);
list = parse_list (header, ',');
for (iter = list; iter; iter = iter->next)
{
g_autofree char *item = iter->data;
eq = strchr (item, '=');
if (eq)
{
name_end = (char *)unskip_lws (eq, item);
if (name_end == item)
continue;
*name_end = '\0';
value = (char *)skip_lws (eq + 1);
if (*value == '"')
decode_quoted_string (value);
}
else
value = NULL;
g_autofree char *key = g_ascii_strdown (item, -1);
if (!g_hash_table_contains (params, key))
g_hash_table_replace (params, g_steal_pointer (&key), g_strdup (value));
}
g_slist_free (list);
return params;
}
/* Do not internationalize */
static const char *const months[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
/* Do not internationalize */
static const char *const days[] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
char *
flatpak_format_http_date (GDateTime *date)
{
g_autoptr(GDateTime) utcdate = g_date_time_to_utc (date);
g_autofree char *date_format = NULL;
/* "Sun, 06 Nov 1994 08:49:37 GMT" */
date_format = g_strdup_printf ("%s, %%d %s %%Y %%T GMT",
days[g_date_time_get_day_of_week (utcdate) - 1],
months[g_date_time_get_month (utcdate) - 1]);
return g_date_time_format (utcdate, (const char*)date_format);
}
static inline gboolean
parse_day (int *day, const char **date_string)
{
char *end;
*day = strtoul (*date_string, &end, 10);
if (end == (char *)*date_string)
return FALSE;
while (*end == ' ' || *end == '-')
end++;
*date_string = end;
return TRUE;
}
static inline gboolean
parse_month (int *month, const char **date_string)
{
int i;
for (i = 0; i < G_N_ELEMENTS (months); i++)
{
if (!g_ascii_strncasecmp (*date_string, months[i], 3))
{
*month = i + 1;
*date_string += 3;
while (**date_string == ' ' || **date_string == '-')
(*date_string)++;
return TRUE;
}
}
return FALSE;
}
static inline gboolean
parse_year (int *year, const char **date_string)
{
char *end;
*year = strtoul (*date_string, &end, 10);
if (end == (char *)*date_string)
return FALSE;
if (end == (char *)*date_string + 2) {
if (*year < 70)
*year += 2000;
else
*year += 1900;
} else if (end == (char *)*date_string + 3)
*year += 1900;
while (*end == ' ' || *end == '-')
end++;
*date_string = end;
return TRUE;
}
static inline gboolean
parse_time (int *hour, int *minute, int *second, const char **date_string)
{
char *p, *end;
*hour = strtoul (*date_string, &end, 10);
if (end == (char *)*date_string || *end++ != ':')
return FALSE;
p = end;
*minute = strtoul (p, &end, 10);
if (end == p || *end++ != ':')
return FALSE;
p = end;
*second = strtoul (p, &end, 10);
if (end == p)
return FALSE;
p = end;
while (*p == ' ')
p++;
*date_string = p;
return TRUE;
}
static inline GTimeZone *
time_zone_new_offset (gint32 offset)
{
#if GLIB_CHECK_VERSION (2, 58, 0)
return g_time_zone_new_offset (offset);
#else
g_autofree char *id = NULL;
gint hours, minutes;
gint seconds = offset;
GTimeZone *tz;
char sign = '+';
if (seconds == 0)
return g_time_zone_new_utc ();
if (seconds < 0)
{
seconds = -seconds;
sign = '-';
}
hours = seconds / 3600;
seconds = seconds % 3600;
minutes = seconds / 60;
seconds = seconds % 60;
id = g_strdup_printf ("%c%02d:%02d:%02d", sign, hours, minutes, seconds);
tz = g_time_zone_new (id);
/* If this assertion fails, we'll log a critical but still return tz,
* which is documented to be UTC if the time zone could not be parsed */
g_return_val_if_fail (g_time_zone_get_offset (tz, 0) == offset, tz);
return tz;
#endif
}
static inline gboolean
parse_timezone (GTimeZone **timezone_out, const char **date_string)
{
gint32 offset_minutes;
gboolean utc;
if (!**date_string)
{
utc = FALSE;
offset_minutes = 0;
}
else if (**date_string == '+' || **date_string == '-')
{
gulong val;
int sign = (**date_string == '+') ? 1 : -1;
val = strtoul (*date_string + 1, (char **)date_string, 10);
if (**date_string == ':')
val = 60 * val + strtoul (*date_string + 1, (char **)date_string, 10);
else
val = 60 * (val / 100) + (val % 100);
offset_minutes = sign * val;
utc = (sign == -1) && !val;
}
else if (**date_string == 'Z')
{
offset_minutes = 0;
utc = TRUE;
(*date_string)++;
}
else if (!strcmp (*date_string, "GMT") ||
!strcmp (*date_string, "UTC"))
{
offset_minutes = 0;
utc = TRUE;
(*date_string) += 3;
}
else if (strchr ("ECMP", **date_string) &&
((*date_string)[1] == 'D' || (*date_string)[1] == 'S') &&
(*date_string)[2] == 'T') {
offset_minutes = -60 * (5 * strcspn ("ECMP", *date_string));
if ((*date_string)[1] == 'D')
offset_minutes += 60;
utc = FALSE;
}
else
return FALSE;
if (utc)
*timezone_out = g_time_zone_new_utc ();
else
*timezone_out = time_zone_new_offset (offset_minutes * 60);
return TRUE;
}
GDateTime *
flatpak_parse_http_time (const char *date_string)
{
int month, day, year, hour, minute, second;
g_autoptr(GTimeZone) tz = NULL;
g_return_val_if_fail (date_string != NULL, NULL);
while (g_ascii_isspace (*date_string))
date_string++;
/* If it starts with a word, it must be a weekday, which we skip */
if (g_ascii_isalpha (*date_string))
{
while (g_ascii_isalpha (*date_string))
date_string++;
if (*date_string == ',')
date_string++;
while (g_ascii_isspace (*date_string))
date_string++;
}
/* If there's now another word, this must be an asctime-date */
if (g_ascii_isalpha (*date_string))
{
/* (Sun) Nov 6 08:49:37 1994 */
if (!parse_month (&month, &date_string) ||
!parse_day (&day, &date_string) ||
!parse_time (&hour, &minute, &second, &date_string) ||
!parse_year (&year, &date_string))
return NULL;
/* There shouldn't be a timezone, but check anyway */
parse_timezone (&tz, &date_string);
}
else
{
/* Non-asctime date, so some variation of
* (Sun,) 06 Nov 1994 08:49:37 GMT
*/
if (!parse_day (&day, &date_string) ||
!parse_month (&month, &date_string) ||
!parse_year (&year, &date_string) ||
!parse_time (&hour, &minute, &second, &date_string))
return NULL;
/* This time there *should* be a timezone, but we
* survive if there isn't.
*/
parse_timezone (&tz, &date_string);
}
if (!tz)
tz = g_time_zone_new_utc ();
return g_date_time_new (tz, year, month, day, hour, minute, second);
}