mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2024-12-27 07:56:14 +01:00
b654eb1846
Make `G_URI_FLAGS_PARSE_RELAXED` available instead, for the implementations which need to handle user-provided or incorrect URIs. The default should nudge people towards being compliant with RFC 3986. This required also adding a new `G_URI_PARAMS_PARSE_RELAXED` flag, as previously parsing param strings *always* used relaxed mode and there was no way to control it. Now it defaults to using strict mode, and the new flag allows for relaxed mode to be enabled if needed. Signed-off-by: Philip Withnall <withnall@endlessm.com> Fixes: #2149
1220 lines
39 KiB
C
1220 lines
39 KiB
C
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
|
||
|
||
/* GIO - GLib Input, Output and Streaming Library
|
||
*
|
||
* Copyright (C) 2008 Red Hat, Inc.
|
||
* Copyright (C) 2018 Igalia S.L.
|
||
*
|
||
* 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.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 <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include "config.h"
|
||
#include <glib.h>
|
||
#include "glibintl.h"
|
||
|
||
#include <stdlib.h>
|
||
#include "gnetworkaddress.h"
|
||
#include "gasyncresult.h"
|
||
#include "ginetaddress.h"
|
||
#include "ginetsocketaddress.h"
|
||
#include "gnetworkingprivate.h"
|
||
#include "gproxyaddressenumerator.h"
|
||
#include "gresolver.h"
|
||
#include "gtask.h"
|
||
#include "gsocketaddressenumerator.h"
|
||
#include "gioerror.h"
|
||
#include "gsocketconnectable.h"
|
||
|
||
#include <string.h>
|
||
|
||
/* As recommended by RFC 8305 this is the time it waits for a following
|
||
DNS response to come in (ipv4 waiting on ipv6 generally)
|
||
*/
|
||
#define HAPPY_EYEBALLS_RESOLUTION_DELAY_MS 50
|
||
|
||
/**
|
||
* SECTION:gnetworkaddress
|
||
* @short_description: A GSocketConnectable for resolving hostnames
|
||
* @include: gio/gio.h
|
||
*
|
||
* #GNetworkAddress provides an easy way to resolve a hostname and
|
||
* then attempt to connect to that host, handling the possibility of
|
||
* multiple IP addresses and multiple address families.
|
||
*
|
||
* The enumeration results of resolved addresses *may* be cached as long
|
||
* as this object is kept alive which may have unexpected results if
|
||
* alive for too long.
|
||
*
|
||
* See #GSocketConnectable for an example of using the connectable
|
||
* interface.
|
||
*/
|
||
|
||
/**
|
||
* GNetworkAddress:
|
||
*
|
||
* A #GSocketConnectable for resolving a hostname and connecting to
|
||
* that host.
|
||
*/
|
||
|
||
struct _GNetworkAddressPrivate {
|
||
gchar *hostname;
|
||
guint16 port;
|
||
GList *cached_sockaddrs;
|
||
gchar *scheme;
|
||
|
||
gint64 resolver_serial;
|
||
};
|
||
|
||
enum {
|
||
PROP_0,
|
||
PROP_HOSTNAME,
|
||
PROP_PORT,
|
||
PROP_SCHEME,
|
||
};
|
||
|
||
static void g_network_address_set_property (GObject *object,
|
||
guint prop_id,
|
||
const GValue *value,
|
||
GParamSpec *pspec);
|
||
static void g_network_address_get_property (GObject *object,
|
||
guint prop_id,
|
||
GValue *value,
|
||
GParamSpec *pspec);
|
||
|
||
static void g_network_address_connectable_iface_init (GSocketConnectableIface *iface);
|
||
static GSocketAddressEnumerator *g_network_address_connectable_enumerate (GSocketConnectable *connectable);
|
||
static GSocketAddressEnumerator *g_network_address_connectable_proxy_enumerate (GSocketConnectable *connectable);
|
||
static gchar *g_network_address_connectable_to_string (GSocketConnectable *connectable);
|
||
|
||
G_DEFINE_TYPE_WITH_CODE (GNetworkAddress, g_network_address, G_TYPE_OBJECT,
|
||
G_ADD_PRIVATE (GNetworkAddress)
|
||
G_IMPLEMENT_INTERFACE (G_TYPE_SOCKET_CONNECTABLE,
|
||
g_network_address_connectable_iface_init))
|
||
|
||
static void
|
||
g_network_address_finalize (GObject *object)
|
||
{
|
||
GNetworkAddress *addr = G_NETWORK_ADDRESS (object);
|
||
|
||
g_free (addr->priv->hostname);
|
||
g_free (addr->priv->scheme);
|
||
g_list_free_full (addr->priv->cached_sockaddrs, g_object_unref);
|
||
|
||
G_OBJECT_CLASS (g_network_address_parent_class)->finalize (object);
|
||
}
|
||
|
||
static void
|
||
g_network_address_class_init (GNetworkAddressClass *klass)
|
||
{
|
||
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||
|
||
gobject_class->set_property = g_network_address_set_property;
|
||
gobject_class->get_property = g_network_address_get_property;
|
||
gobject_class->finalize = g_network_address_finalize;
|
||
|
||
g_object_class_install_property (gobject_class, PROP_HOSTNAME,
|
||
g_param_spec_string ("hostname",
|
||
P_("Hostname"),
|
||
P_("Hostname to resolve"),
|
||
NULL,
|
||
G_PARAM_READWRITE |
|
||
G_PARAM_CONSTRUCT_ONLY |
|
||
G_PARAM_STATIC_STRINGS));
|
||
g_object_class_install_property (gobject_class, PROP_PORT,
|
||
g_param_spec_uint ("port",
|
||
P_("Port"),
|
||
P_("Network port"),
|
||
0, 65535, 0,
|
||
G_PARAM_READWRITE |
|
||
G_PARAM_CONSTRUCT_ONLY |
|
||
G_PARAM_STATIC_STRINGS));
|
||
|
||
g_object_class_install_property (gobject_class, PROP_SCHEME,
|
||
g_param_spec_string ("scheme",
|
||
P_("Scheme"),
|
||
P_("URI Scheme"),
|
||
NULL,
|
||
G_PARAM_READWRITE |
|
||
G_PARAM_CONSTRUCT_ONLY |
|
||
G_PARAM_STATIC_STRINGS));
|
||
}
|
||
|
||
static void
|
||
g_network_address_connectable_iface_init (GSocketConnectableIface *connectable_iface)
|
||
{
|
||
connectable_iface->enumerate = g_network_address_connectable_enumerate;
|
||
connectable_iface->proxy_enumerate = g_network_address_connectable_proxy_enumerate;
|
||
connectable_iface->to_string = g_network_address_connectable_to_string;
|
||
}
|
||
|
||
static void
|
||
g_network_address_init (GNetworkAddress *addr)
|
||
{
|
||
addr->priv = g_network_address_get_instance_private (addr);
|
||
}
|
||
|
||
static void
|
||
g_network_address_set_property (GObject *object,
|
||
guint prop_id,
|
||
const GValue *value,
|
||
GParamSpec *pspec)
|
||
{
|
||
GNetworkAddress *addr = G_NETWORK_ADDRESS (object);
|
||
|
||
switch (prop_id)
|
||
{
|
||
case PROP_HOSTNAME:
|
||
g_free (addr->priv->hostname);
|
||
addr->priv->hostname = g_value_dup_string (value);
|
||
break;
|
||
|
||
case PROP_PORT:
|
||
addr->priv->port = g_value_get_uint (value);
|
||
break;
|
||
|
||
case PROP_SCHEME:
|
||
g_free (addr->priv->scheme);
|
||
addr->priv->scheme = g_value_dup_string (value);
|
||
break;
|
||
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
static void
|
||
g_network_address_get_property (GObject *object,
|
||
guint prop_id,
|
||
GValue *value,
|
||
GParamSpec *pspec)
|
||
{
|
||
GNetworkAddress *addr = G_NETWORK_ADDRESS (object);
|
||
|
||
switch (prop_id)
|
||
{
|
||
case PROP_HOSTNAME:
|
||
g_value_set_string (value, addr->priv->hostname);
|
||
break;
|
||
|
||
case PROP_PORT:
|
||
g_value_set_uint (value, addr->priv->port);
|
||
break;
|
||
|
||
case PROP_SCHEME:
|
||
g_value_set_string (value, addr->priv->scheme);
|
||
break;
|
||
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
/*
|
||
* inet_addresses_to_inet_socket_addresses:
|
||
* @addresses: (transfer full): #GList of #GInetAddress
|
||
*
|
||
* Returns: (transfer full): #GList of #GInetSocketAddress
|
||
*/
|
||
static GList *
|
||
inet_addresses_to_inet_socket_addresses (GNetworkAddress *addr,
|
||
GList *addresses)
|
||
{
|
||
GList *a, *socket_addresses = NULL;
|
||
|
||
for (a = addresses; a; a = a->next)
|
||
{
|
||
GSocketAddress *sockaddr = g_inet_socket_address_new (a->data, addr->priv->port);
|
||
socket_addresses = g_list_append (socket_addresses, g_steal_pointer (&sockaddr));
|
||
g_object_unref (a->data);
|
||
}
|
||
|
||
g_list_free (addresses);
|
||
return socket_addresses;
|
||
}
|
||
|
||
/*
|
||
* g_network_address_set_cached_addresses:
|
||
* @addr: A #GNetworkAddress
|
||
* @addresses: (transfer full): List of #GInetAddress or #GInetSocketAddress
|
||
* @resolver_serial: Serial of #GResolver used
|
||
*
|
||
* Consumes @addresses and uses them to replace the current internal list.
|
||
*/
|
||
static void
|
||
g_network_address_set_cached_addresses (GNetworkAddress *addr,
|
||
GList *addresses,
|
||
guint64 resolver_serial)
|
||
{
|
||
g_assert (addresses != NULL);
|
||
|
||
if (addr->priv->cached_sockaddrs)
|
||
g_list_free_full (addr->priv->cached_sockaddrs, g_object_unref);
|
||
|
||
if (G_IS_INET_SOCKET_ADDRESS (addresses->data))
|
||
addr->priv->cached_sockaddrs = g_steal_pointer (&addresses);
|
||
else
|
||
addr->priv->cached_sockaddrs = inet_addresses_to_inet_socket_addresses (addr, g_steal_pointer (&addresses));
|
||
addr->priv->resolver_serial = resolver_serial;
|
||
}
|
||
|
||
static gboolean
|
||
g_network_address_parse_sockaddr (GNetworkAddress *addr)
|
||
{
|
||
GSocketAddress *sockaddr;
|
||
|
||
g_assert (addr->priv->cached_sockaddrs == NULL);
|
||
|
||
sockaddr = g_inet_socket_address_new_from_string (addr->priv->hostname,
|
||
addr->priv->port);
|
||
if (sockaddr)
|
||
{
|
||
addr->priv->cached_sockaddrs = g_list_append (addr->priv->cached_sockaddrs, sockaddr);
|
||
return TRUE;
|
||
}
|
||
else
|
||
return FALSE;
|
||
}
|
||
|
||
/**
|
||
* g_network_address_new:
|
||
* @hostname: the hostname
|
||
* @port: the port
|
||
*
|
||
* Creates a new #GSocketConnectable for connecting to the given
|
||
* @hostname and @port.
|
||
*
|
||
* Note that depending on the configuration of the machine, a
|
||
* @hostname of `localhost` may refer to the IPv4 loopback address
|
||
* only, or to both IPv4 and IPv6; use
|
||
* g_network_address_new_loopback() to create a #GNetworkAddress that
|
||
* is guaranteed to resolve to both addresses.
|
||
*
|
||
* Returns: (transfer full) (type GNetworkAddress): the new #GNetworkAddress
|
||
*
|
||
* Since: 2.22
|
||
*/
|
||
GSocketConnectable *
|
||
g_network_address_new (const gchar *hostname,
|
||
guint16 port)
|
||
{
|
||
return g_object_new (G_TYPE_NETWORK_ADDRESS,
|
||
"hostname", hostname,
|
||
"port", port,
|
||
NULL);
|
||
}
|
||
|
||
/**
|
||
* g_network_address_new_loopback:
|
||
* @port: the port
|
||
*
|
||
* Creates a new #GSocketConnectable for connecting to the local host
|
||
* over a loopback connection to the given @port. This is intended for
|
||
* use in connecting to local services which may be running on IPv4 or
|
||
* IPv6.
|
||
*
|
||
* The connectable will return IPv4 and IPv6 loopback addresses,
|
||
* regardless of how the host resolves `localhost`. By contrast,
|
||
* g_network_address_new() will often only return an IPv4 address when
|
||
* resolving `localhost`, and an IPv6 address for `localhost6`.
|
||
*
|
||
* g_network_address_get_hostname() will always return `localhost` for
|
||
* a #GNetworkAddress created with this constructor.
|
||
*
|
||
* Returns: (transfer full) (type GNetworkAddress): the new #GNetworkAddress
|
||
*
|
||
* Since: 2.44
|
||
*/
|
||
GSocketConnectable *
|
||
g_network_address_new_loopback (guint16 port)
|
||
{
|
||
GNetworkAddress *addr;
|
||
GList *addrs = NULL;
|
||
|
||
addr = g_object_new (G_TYPE_NETWORK_ADDRESS,
|
||
"hostname", "localhost",
|
||
"port", port,
|
||
NULL);
|
||
|
||
addrs = g_list_append (addrs, g_inet_address_new_loopback (AF_INET6));
|
||
addrs = g_list_append (addrs, g_inet_address_new_loopback (AF_INET));
|
||
g_network_address_set_cached_addresses (addr, g_steal_pointer (&addrs), 0);
|
||
|
||
return G_SOCKET_CONNECTABLE (addr);
|
||
}
|
||
|
||
/**
|
||
* g_network_address_parse:
|
||
* @host_and_port: the hostname and optionally a port
|
||
* @default_port: the default port if not in @host_and_port
|
||
* @error: a pointer to a #GError, or %NULL
|
||
*
|
||
* Creates a new #GSocketConnectable for connecting to the given
|
||
* @hostname and @port. May fail and return %NULL in case
|
||
* parsing @host_and_port fails.
|
||
*
|
||
* @host_and_port may be in any of a number of recognised formats; an IPv6
|
||
* address, an IPv4 address, or a domain name (in which case a DNS
|
||
* lookup is performed). Quoting with [] is supported for all address
|
||
* types. A port override may be specified in the usual way with a
|
||
* colon.
|
||
*
|
||
* If no port is specified in @host_and_port then @default_port will be
|
||
* used as the port number to connect to.
|
||
*
|
||
* In general, @host_and_port is expected to be provided by the user
|
||
* (allowing them to give the hostname, and a port override if necessary)
|
||
* and @default_port is expected to be provided by the application.
|
||
*
|
||
* (The port component of @host_and_port can also be specified as a
|
||
* service name rather than as a numeric port, but this functionality
|
||
* is deprecated, because it depends on the contents of /etc/services,
|
||
* which is generally quite sparse on platforms other than Linux.)
|
||
*
|
||
* Returns: (transfer full) (type GNetworkAddress): the new
|
||
* #GNetworkAddress, or %NULL on error
|
||
*
|
||
* Since: 2.22
|
||
*/
|
||
GSocketConnectable *
|
||
g_network_address_parse (const gchar *host_and_port,
|
||
guint16 default_port,
|
||
GError **error)
|
||
{
|
||
GSocketConnectable *connectable;
|
||
const gchar *port;
|
||
guint16 portnum;
|
||
gchar *name;
|
||
|
||
g_return_val_if_fail (host_and_port != NULL, NULL);
|
||
|
||
port = NULL;
|
||
if (host_and_port[0] == '[')
|
||
/* escaped host part (to allow, eg. "[2001:db8::1]:888") */
|
||
{
|
||
const gchar *end;
|
||
|
||
end = strchr (host_and_port, ']');
|
||
if (end == NULL)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
_("Hostname “%s” contains “[” but not “]”"), host_and_port);
|
||
return NULL;
|
||
}
|
||
|
||
if (end[1] == '\0')
|
||
port = NULL;
|
||
else if (end[1] == ':')
|
||
port = &end[2];
|
||
else
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
"The ']' character (in hostname '%s') must come at the"
|
||
" end or be immediately followed by ':' and a port",
|
||
host_and_port);
|
||
return NULL;
|
||
}
|
||
|
||
name = g_strndup (host_and_port + 1, end - host_and_port - 1);
|
||
}
|
||
|
||
else if ((port = strchr (host_and_port, ':')))
|
||
/* string has a ':' in it */
|
||
{
|
||
/* skip ':' */
|
||
port++;
|
||
|
||
if (strchr (port, ':'))
|
||
/* more than one ':' in string */
|
||
{
|
||
/* this is actually an unescaped IPv6 address */
|
||
name = g_strdup (host_and_port);
|
||
port = NULL;
|
||
}
|
||
else
|
||
name = g_strndup (host_and_port, port - host_and_port - 1);
|
||
}
|
||
|
||
else
|
||
/* plain hostname, no port */
|
||
name = g_strdup (host_and_port);
|
||
|
||
if (port != NULL)
|
||
{
|
||
if (port[0] == '\0')
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
"If a ':' character is given, it must be followed by a "
|
||
"port (in hostname '%s').", host_and_port);
|
||
g_free (name);
|
||
return NULL;
|
||
}
|
||
|
||
else if ('0' <= port[0] && port[0] <= '9')
|
||
{
|
||
char *end;
|
||
long value;
|
||
|
||
value = strtol (port, &end, 10);
|
||
if (*end != '\0' || value < 0 || value > G_MAXUINT16)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
"Invalid numeric port '%s' specified in hostname '%s'",
|
||
port, host_and_port);
|
||
g_free (name);
|
||
return NULL;
|
||
}
|
||
|
||
portnum = value;
|
||
}
|
||
|
||
else
|
||
{
|
||
struct servent *entry;
|
||
|
||
entry = getservbyname (port, "tcp");
|
||
if (entry == NULL)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
"Unknown service '%s' specified in hostname '%s'",
|
||
port, host_and_port);
|
||
#ifdef HAVE_ENDSERVENT
|
||
endservent ();
|
||
#endif
|
||
g_free (name);
|
||
return NULL;
|
||
}
|
||
|
||
portnum = g_ntohs (entry->s_port);
|
||
|
||
#ifdef HAVE_ENDSERVENT
|
||
endservent ();
|
||
#endif
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* No port in host_and_port */
|
||
portnum = default_port;
|
||
}
|
||
|
||
connectable = g_network_address_new (name, portnum);
|
||
g_free (name);
|
||
|
||
return connectable;
|
||
}
|
||
|
||
/**
|
||
* g_network_address_parse_uri:
|
||
* @uri: the hostname and optionally a port
|
||
* @default_port: The default port if none is found in the URI
|
||
* @error: a pointer to a #GError, or %NULL
|
||
*
|
||
* Creates a new #GSocketConnectable for connecting to the given
|
||
* @uri. May fail and return %NULL in case parsing @uri fails.
|
||
*
|
||
* Using this rather than g_network_address_new() or
|
||
* g_network_address_parse() allows #GSocketClient to determine
|
||
* when to use application-specific proxy protocols.
|
||
*
|
||
* Returns: (transfer full) (type GNetworkAddress): the new
|
||
* #GNetworkAddress, or %NULL on error
|
||
*
|
||
* Since: 2.26
|
||
*/
|
||
GSocketConnectable *
|
||
g_network_address_parse_uri (const gchar *uri,
|
||
guint16 default_port,
|
||
GError **error)
|
||
{
|
||
GSocketConnectable *conn = NULL;
|
||
gchar *scheme = NULL;
|
||
gchar *hostname = NULL;
|
||
gint port;
|
||
|
||
if (!g_uri_split_network (uri, G_URI_FLAGS_NONE,
|
||
&scheme, &hostname, &port, NULL))
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
"Invalid URI ‘%s’", uri);
|
||
return NULL;
|
||
}
|
||
|
||
if (port <= 0)
|
||
port = default_port;
|
||
|
||
conn = g_object_new (G_TYPE_NETWORK_ADDRESS,
|
||
"hostname", hostname,
|
||
"port", (guint) port,
|
||
"scheme", scheme,
|
||
NULL);
|
||
g_free (scheme);
|
||
g_free (hostname);
|
||
|
||
return conn;
|
||
}
|
||
|
||
/**
|
||
* g_network_address_get_hostname:
|
||
* @addr: a #GNetworkAddress
|
||
*
|
||
* Gets @addr's hostname. This might be either UTF-8 or ASCII-encoded,
|
||
* depending on what @addr was created with.
|
||
*
|
||
* Returns: @addr's hostname
|
||
*
|
||
* Since: 2.22
|
||
*/
|
||
const gchar *
|
||
g_network_address_get_hostname (GNetworkAddress *addr)
|
||
{
|
||
g_return_val_if_fail (G_IS_NETWORK_ADDRESS (addr), NULL);
|
||
|
||
return addr->priv->hostname;
|
||
}
|
||
|
||
/**
|
||
* g_network_address_get_port:
|
||
* @addr: a #GNetworkAddress
|
||
*
|
||
* Gets @addr's port number
|
||
*
|
||
* Returns: @addr's port (which may be 0)
|
||
*
|
||
* Since: 2.22
|
||
*/
|
||
guint16
|
||
g_network_address_get_port (GNetworkAddress *addr)
|
||
{
|
||
g_return_val_if_fail (G_IS_NETWORK_ADDRESS (addr), 0);
|
||
|
||
return addr->priv->port;
|
||
}
|
||
|
||
/**
|
||
* g_network_address_get_scheme:
|
||
* @addr: a #GNetworkAddress
|
||
*
|
||
* Gets @addr's scheme
|
||
*
|
||
* Returns: @addr's scheme (%NULL if not built from URI)
|
||
*
|
||
* Since: 2.26
|
||
*/
|
||
const gchar *
|
||
g_network_address_get_scheme (GNetworkAddress *addr)
|
||
{
|
||
g_return_val_if_fail (G_IS_NETWORK_ADDRESS (addr), NULL);
|
||
|
||
return addr->priv->scheme;
|
||
}
|
||
|
||
#define G_TYPE_NETWORK_ADDRESS_ADDRESS_ENUMERATOR (_g_network_address_address_enumerator_get_type ())
|
||
#define G_NETWORK_ADDRESS_ADDRESS_ENUMERATOR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_NETWORK_ADDRESS_ADDRESS_ENUMERATOR, GNetworkAddressAddressEnumerator))
|
||
|
||
typedef enum {
|
||
RESOLVE_STATE_NONE = 0,
|
||
RESOLVE_STATE_WAITING_ON_IPV4 = 1 << 0,
|
||
RESOLVE_STATE_WAITING_ON_IPV6 = 1 << 1,
|
||
} ResolveState;
|
||
|
||
typedef struct {
|
||
GSocketAddressEnumerator parent_instance;
|
||
|
||
GNetworkAddress *addr; /* (owned) */
|
||
GList *addresses; /* (owned) (nullable) */
|
||
GList *current_item; /* (unowned) (nullable) */
|
||
GTask *queued_task; /* (owned) (nullable) */
|
||
GTask *waiting_task; /* (owned) (nullable) */
|
||
GError *last_error; /* (owned) (nullable) */
|
||
GSource *wait_source; /* (owned) (nullable) */
|
||
GMainContext *context; /* (owned) (nullable) */
|
||
ResolveState state;
|
||
} GNetworkAddressAddressEnumerator;
|
||
|
||
typedef struct {
|
||
GSocketAddressEnumeratorClass parent_class;
|
||
|
||
} GNetworkAddressAddressEnumeratorClass;
|
||
|
||
static GType _g_network_address_address_enumerator_get_type (void);
|
||
G_DEFINE_TYPE (GNetworkAddressAddressEnumerator, _g_network_address_address_enumerator, G_TYPE_SOCKET_ADDRESS_ENUMERATOR)
|
||
|
||
static void
|
||
g_network_address_address_enumerator_finalize (GObject *object)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum =
|
||
G_NETWORK_ADDRESS_ADDRESS_ENUMERATOR (object);
|
||
|
||
if (addr_enum->wait_source)
|
||
{
|
||
g_source_destroy (addr_enum->wait_source);
|
||
g_clear_pointer (&addr_enum->wait_source, g_source_unref);
|
||
}
|
||
g_clear_object (&addr_enum->queued_task);
|
||
g_clear_object (&addr_enum->waiting_task);
|
||
g_clear_error (&addr_enum->last_error);
|
||
g_object_unref (addr_enum->addr);
|
||
g_clear_pointer (&addr_enum->context, g_main_context_unref);
|
||
g_list_free_full (addr_enum->addresses, g_object_unref);
|
||
|
||
G_OBJECT_CLASS (_g_network_address_address_enumerator_parent_class)->finalize (object);
|
||
}
|
||
|
||
static inline GSocketFamily
|
||
get_address_family (GInetSocketAddress *address)
|
||
{
|
||
return g_inet_address_get_family (g_inet_socket_address_get_address (address));
|
||
}
|
||
|
||
static void
|
||
list_split_families (GList *list,
|
||
GList **out_ipv4,
|
||
GList **out_ipv6)
|
||
{
|
||
g_assert (out_ipv4);
|
||
g_assert (out_ipv6);
|
||
|
||
while (list)
|
||
{
|
||
GSocketFamily family = get_address_family (list->data);
|
||
switch (family)
|
||
{
|
||
case G_SOCKET_FAMILY_IPV4:
|
||
*out_ipv4 = g_list_prepend (*out_ipv4, list->data);
|
||
break;
|
||
case G_SOCKET_FAMILY_IPV6:
|
||
*out_ipv6 = g_list_prepend (*out_ipv6, list->data);
|
||
break;
|
||
case G_SOCKET_FAMILY_INVALID:
|
||
case G_SOCKET_FAMILY_UNIX:
|
||
g_assert_not_reached ();
|
||
}
|
||
|
||
list = g_list_next (list);
|
||
}
|
||
|
||
*out_ipv4 = g_list_reverse (*out_ipv4);
|
||
*out_ipv6 = g_list_reverse (*out_ipv6);
|
||
}
|
||
|
||
static GList *
|
||
list_interleave_families (GList *list1,
|
||
GList *list2)
|
||
{
|
||
GList *interleaved = NULL;
|
||
|
||
while (list1 || list2)
|
||
{
|
||
if (list1)
|
||
{
|
||
interleaved = g_list_append (interleaved, list1->data);
|
||
list1 = g_list_delete_link (list1, list1);
|
||
}
|
||
if (list2)
|
||
{
|
||
interleaved = g_list_append (interleaved, list2->data);
|
||
list2 = g_list_delete_link (list2, list2);
|
||
}
|
||
}
|
||
|
||
return interleaved;
|
||
}
|
||
|
||
/* list_copy_interleaved:
|
||
* @list: (transfer container): List to copy
|
||
*
|
||
* Does a shallow copy of a list with address families interleaved.
|
||
*
|
||
* For example:
|
||
* Input: [ipv6, ipv6, ipv4, ipv4]
|
||
* Output: [ipv6, ipv4, ipv6, ipv4]
|
||
*
|
||
* Returns: (transfer container): A new list
|
||
*/
|
||
static GList *
|
||
list_copy_interleaved (GList *list)
|
||
{
|
||
GList *ipv4 = NULL, *ipv6 = NULL;
|
||
|
||
list_split_families (list, &ipv4, &ipv6);
|
||
return list_interleave_families (ipv6, ipv4);
|
||
}
|
||
|
||
/* list_concat_interleaved:
|
||
* @parent_list: (transfer container): Already existing list
|
||
* @current_item: (transfer container): Item after which to resort
|
||
* @new_list: (transfer container): New list to be interleaved and concatenated
|
||
*
|
||
* This differs from g_list_concat() + list_copy_interleaved() in that it sorts
|
||
* items in the previous list starting from @current_item and concats the results
|
||
* to @parent_list.
|
||
*
|
||
* Returns: (transfer container): New start of list
|
||
*/
|
||
static GList *
|
||
list_concat_interleaved (GList *parent_list,
|
||
GList *current_item,
|
||
GList *new_list)
|
||
{
|
||
GList *ipv4 = NULL, *ipv6 = NULL, *interleaved, *trailing = NULL;
|
||
GSocketFamily last_family = G_SOCKET_FAMILY_IPV4; /* Default to starting with ipv6 */
|
||
|
||
if (current_item)
|
||
{
|
||
last_family = get_address_family (current_item->data);
|
||
|
||
/* Unused addresses will get removed, resorted, then readded */
|
||
trailing = g_list_next (current_item);
|
||
current_item->next = NULL;
|
||
}
|
||
|
||
list_split_families (trailing, &ipv4, &ipv6);
|
||
list_split_families (new_list, &ipv4, &ipv6);
|
||
g_list_free (new_list);
|
||
|
||
if (trailing)
|
||
g_list_free (trailing);
|
||
|
||
if (last_family == G_SOCKET_FAMILY_IPV4)
|
||
interleaved = list_interleave_families (ipv6, ipv4);
|
||
else
|
||
interleaved = list_interleave_families (ipv4, ipv6);
|
||
|
||
return g_list_concat (parent_list, interleaved);
|
||
}
|
||
|
||
static void
|
||
maybe_update_address_cache (GNetworkAddressAddressEnumerator *addr_enum,
|
||
GResolver *resolver)
|
||
{
|
||
GList *addresses, *p;
|
||
|
||
/* Only cache complete results */
|
||
if (addr_enum->state & RESOLVE_STATE_WAITING_ON_IPV4 || addr_enum->state & RESOLVE_STATE_WAITING_ON_IPV6)
|
||
return;
|
||
|
||
/* The enumerators list will not necessarily be fully sorted */
|
||
addresses = list_copy_interleaved (addr_enum->addresses);
|
||
for (p = addresses; p; p = p->next)
|
||
g_object_ref (p->data);
|
||
|
||
g_network_address_set_cached_addresses (addr_enum->addr, g_steal_pointer (&addresses), g_resolver_get_serial (resolver));
|
||
}
|
||
|
||
static void
|
||
g_network_address_address_enumerator_add_addresses (GNetworkAddressAddressEnumerator *addr_enum,
|
||
GList *addresses,
|
||
GResolver *resolver)
|
||
{
|
||
GList *new_addresses = inet_addresses_to_inet_socket_addresses (addr_enum->addr, addresses);
|
||
|
||
if (addr_enum->addresses == NULL)
|
||
addr_enum->addresses = g_steal_pointer (&new_addresses);
|
||
else
|
||
addr_enum->addresses = list_concat_interleaved (addr_enum->addresses, addr_enum->current_item, g_steal_pointer (&new_addresses));
|
||
|
||
maybe_update_address_cache (addr_enum, resolver);
|
||
}
|
||
|
||
static gpointer
|
||
copy_object (gconstpointer src,
|
||
gpointer user_data)
|
||
{
|
||
return g_object_ref (G_OBJECT (src));
|
||
}
|
||
|
||
static GSocketAddress *
|
||
init_and_query_next_address (GNetworkAddressAddressEnumerator *addr_enum)
|
||
{
|
||
GList *next_item;
|
||
|
||
if (addr_enum->addresses == NULL)
|
||
addr_enum->addresses = g_list_copy_deep (addr_enum->addr->priv->cached_sockaddrs,
|
||
copy_object, NULL);
|
||
|
||
/* We always want to look at the next item at call time to get the latest results.
|
||
That means that sometimes ->next is NULL this call but is valid next call.
|
||
*/
|
||
if (addr_enum->current_item == NULL)
|
||
next_item = addr_enum->current_item = addr_enum->addresses;
|
||
else
|
||
next_item = g_list_next (addr_enum->current_item);
|
||
|
||
if (next_item)
|
||
{
|
||
addr_enum->current_item = next_item;
|
||
return g_object_ref (addr_enum->current_item->data);
|
||
}
|
||
else
|
||
return NULL;
|
||
}
|
||
|
||
static GSocketAddress *
|
||
g_network_address_address_enumerator_next (GSocketAddressEnumerator *enumerator,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum =
|
||
G_NETWORK_ADDRESS_ADDRESS_ENUMERATOR (enumerator);
|
||
|
||
if (addr_enum->addresses == NULL)
|
||
{
|
||
GNetworkAddress *addr = addr_enum->addr;
|
||
GResolver *resolver = g_resolver_get_default ();
|
||
gint64 serial = g_resolver_get_serial (resolver);
|
||
|
||
if (addr->priv->resolver_serial != 0 &&
|
||
addr->priv->resolver_serial != serial)
|
||
{
|
||
/* Resolver has reloaded, discard cached addresses */
|
||
g_list_free_full (addr->priv->cached_sockaddrs, g_object_unref);
|
||
addr->priv->cached_sockaddrs = NULL;
|
||
}
|
||
|
||
if (!addr->priv->cached_sockaddrs)
|
||
g_network_address_parse_sockaddr (addr);
|
||
if (!addr->priv->cached_sockaddrs)
|
||
{
|
||
GList *addresses;
|
||
|
||
addresses = g_resolver_lookup_by_name (resolver,
|
||
addr->priv->hostname,
|
||
cancellable, error);
|
||
if (!addresses)
|
||
{
|
||
g_object_unref (resolver);
|
||
return NULL;
|
||
}
|
||
|
||
g_network_address_set_cached_addresses (addr, g_steal_pointer (&addresses), serial);
|
||
}
|
||
|
||
g_object_unref (resolver);
|
||
}
|
||
|
||
return init_and_query_next_address (addr_enum);
|
||
}
|
||
|
||
static void
|
||
complete_queued_task (GNetworkAddressAddressEnumerator *addr_enum,
|
||
GTask *task,
|
||
GError *error)
|
||
{
|
||
if (error)
|
||
g_task_return_error (task, error);
|
||
else
|
||
{
|
||
GSocketAddress *sockaddr = init_and_query_next_address (addr_enum);
|
||
g_task_return_pointer (task, g_steal_pointer (&sockaddr), g_object_unref);
|
||
}
|
||
g_object_unref (task);
|
||
}
|
||
|
||
static int
|
||
on_address_timeout (gpointer user_data)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum = user_data;
|
||
|
||
/* Upon completion it may get unref'd by the owner */
|
||
g_object_ref (addr_enum);
|
||
|
||
if (addr_enum->queued_task != NULL)
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->queued_task),
|
||
g_steal_pointer (&addr_enum->last_error));
|
||
else if (addr_enum->waiting_task != NULL)
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->waiting_task),
|
||
NULL);
|
||
|
||
g_clear_pointer (&addr_enum->wait_source, g_source_unref);
|
||
g_object_unref (addr_enum);
|
||
|
||
return G_SOURCE_REMOVE;
|
||
}
|
||
|
||
static void
|
||
got_ipv6_addresses (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum = user_data;
|
||
GResolver *resolver = G_RESOLVER (source_object);
|
||
GList *addresses;
|
||
GError *error = NULL;
|
||
|
||
addr_enum->state ^= RESOLVE_STATE_WAITING_ON_IPV6;
|
||
|
||
addresses = g_resolver_lookup_by_name_with_flags_finish (resolver, result, &error);
|
||
if (!error)
|
||
g_network_address_address_enumerator_add_addresses (addr_enum, g_steal_pointer (&addresses), resolver);
|
||
else
|
||
g_debug ("IPv6 DNS error: %s", error->message);
|
||
|
||
/* If ipv4 was first and waiting on us it can stop waiting */
|
||
if (addr_enum->wait_source)
|
||
{
|
||
g_source_destroy (addr_enum->wait_source);
|
||
g_clear_pointer (&addr_enum->wait_source, g_source_unref);
|
||
}
|
||
|
||
/* If we got an error before ipv4 then let its response handle it.
|
||
* If we get ipv6 response first or error second then
|
||
* immediately complete the task.
|
||
*/
|
||
if (error != NULL && !addr_enum->last_error && (addr_enum->state & RESOLVE_STATE_WAITING_ON_IPV4))
|
||
{
|
||
/* ipv6 lookup failed, but ipv4 is still outstanding. wait. */
|
||
addr_enum->last_error = g_steal_pointer (&error);
|
||
}
|
||
else if (addr_enum->waiting_task != NULL)
|
||
{
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->waiting_task), NULL);
|
||
}
|
||
else if (addr_enum->queued_task != NULL)
|
||
{
|
||
GError *task_error = NULL;
|
||
|
||
/* If both errored just use the ipv6 one,
|
||
but if ipv6 errored and ipv4 didn't we don't error */
|
||
if (error != NULL && addr_enum->last_error)
|
||
task_error = g_steal_pointer (&error);
|
||
|
||
g_clear_error (&addr_enum->last_error);
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->queued_task),
|
||
g_steal_pointer (&task_error));
|
||
}
|
||
|
||
g_clear_error (&error);
|
||
g_object_unref (addr_enum);
|
||
}
|
||
|
||
static void
|
||
got_ipv4_addresses (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum = user_data;
|
||
GResolver *resolver = G_RESOLVER (source_object);
|
||
GList *addresses;
|
||
GError *error = NULL;
|
||
|
||
addr_enum->state ^= RESOLVE_STATE_WAITING_ON_IPV4;
|
||
|
||
addresses = g_resolver_lookup_by_name_with_flags_finish (resolver, result, &error);
|
||
if (!error)
|
||
g_network_address_address_enumerator_add_addresses (addr_enum, g_steal_pointer (&addresses), resolver);
|
||
else
|
||
g_debug ("IPv4 DNS error: %s", error->message);
|
||
|
||
if (addr_enum->wait_source)
|
||
{
|
||
g_source_destroy (addr_enum->wait_source);
|
||
g_clear_pointer (&addr_enum->wait_source, g_source_unref);
|
||
}
|
||
|
||
/* If ipv6 already came in and errored then we return.
|
||
* If ipv6 returned successfully then we don't need to do anything unless
|
||
* another enumeration was waiting on us.
|
||
* If ipv6 hasn't come we should wait a short while for it as RFC 8305 suggests.
|
||
*/
|
||
if (addr_enum->last_error)
|
||
{
|
||
g_assert (addr_enum->queued_task);
|
||
g_clear_error (&addr_enum->last_error);
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->queued_task),
|
||
g_steal_pointer (&error));
|
||
}
|
||
else if (addr_enum->waiting_task != NULL)
|
||
{
|
||
complete_queued_task (addr_enum, g_steal_pointer (&addr_enum->waiting_task), NULL);
|
||
}
|
||
else if (addr_enum->queued_task != NULL)
|
||
{
|
||
addr_enum->last_error = g_steal_pointer (&error);
|
||
addr_enum->wait_source = g_timeout_source_new (HAPPY_EYEBALLS_RESOLUTION_DELAY_MS);
|
||
g_source_set_callback (addr_enum->wait_source,
|
||
on_address_timeout,
|
||
addr_enum, NULL);
|
||
g_source_attach (addr_enum->wait_source, addr_enum->context);
|
||
}
|
||
|
||
g_clear_error (&error);
|
||
g_object_unref (addr_enum);
|
||
}
|
||
|
||
static void
|
||
g_network_address_address_enumerator_next_async (GSocketAddressEnumerator *enumerator,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum =
|
||
G_NETWORK_ADDRESS_ADDRESS_ENUMERATOR (enumerator);
|
||
GSocketAddress *sockaddr;
|
||
GTask *task;
|
||
|
||
task = g_task_new (addr_enum, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, g_network_address_address_enumerator_next_async);
|
||
|
||
if (addr_enum->addresses == NULL && addr_enum->state == RESOLVE_STATE_NONE)
|
||
{
|
||
GNetworkAddress *addr = addr_enum->addr;
|
||
GResolver *resolver = g_resolver_get_default ();
|
||
gint64 serial = g_resolver_get_serial (resolver);
|
||
|
||
if (addr->priv->resolver_serial != 0 &&
|
||
addr->priv->resolver_serial != serial)
|
||
{
|
||
/* Resolver has reloaded, discard cached addresses */
|
||
g_list_free_full (addr->priv->cached_sockaddrs, g_object_unref);
|
||
addr->priv->cached_sockaddrs = NULL;
|
||
}
|
||
|
||
if (addr->priv->cached_sockaddrs == NULL)
|
||
{
|
||
if (g_network_address_parse_sockaddr (addr))
|
||
complete_queued_task (addr_enum, task, NULL);
|
||
else
|
||
{
|
||
/* It does not make sense for this to be called multiple
|
||
* times before the initial callback has been called */
|
||
g_assert (addr_enum->queued_task == NULL);
|
||
|
||
addr_enum->state = RESOLVE_STATE_WAITING_ON_IPV4 | RESOLVE_STATE_WAITING_ON_IPV6;
|
||
addr_enum->queued_task = g_steal_pointer (&task);
|
||
/* Look up in parallel as per RFC 8305 */
|
||
g_resolver_lookup_by_name_with_flags_async (resolver,
|
||
addr->priv->hostname,
|
||
G_RESOLVER_NAME_LOOKUP_FLAGS_IPV6_ONLY,
|
||
cancellable,
|
||
got_ipv6_addresses, g_object_ref (addr_enum));
|
||
g_resolver_lookup_by_name_with_flags_async (resolver,
|
||
addr->priv->hostname,
|
||
G_RESOLVER_NAME_LOOKUP_FLAGS_IPV4_ONLY,
|
||
cancellable,
|
||
got_ipv4_addresses, g_object_ref (addr_enum));
|
||
}
|
||
g_object_unref (resolver);
|
||
return;
|
||
}
|
||
|
||
g_object_unref (resolver);
|
||
}
|
||
|
||
sockaddr = init_and_query_next_address (addr_enum);
|
||
if (sockaddr == NULL && (addr_enum->state & RESOLVE_STATE_WAITING_ON_IPV4 ||
|
||
addr_enum->state & RESOLVE_STATE_WAITING_ON_IPV6))
|
||
{
|
||
addr_enum->waiting_task = task;
|
||
}
|
||
else
|
||
{
|
||
g_task_return_pointer (task, sockaddr, g_object_unref);
|
||
g_object_unref (task);
|
||
}
|
||
}
|
||
|
||
static GSocketAddress *
|
||
g_network_address_address_enumerator_next_finish (GSocketAddressEnumerator *enumerator,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
g_return_val_if_fail (g_task_is_valid (result, enumerator), NULL);
|
||
|
||
return g_task_propagate_pointer (G_TASK (result), error);
|
||
}
|
||
|
||
static void
|
||
_g_network_address_address_enumerator_init (GNetworkAddressAddressEnumerator *enumerator)
|
||
{
|
||
enumerator->context = g_main_context_ref_thread_default ();
|
||
}
|
||
|
||
static void
|
||
_g_network_address_address_enumerator_class_init (GNetworkAddressAddressEnumeratorClass *addrenum_class)
|
||
{
|
||
GObjectClass *object_class = G_OBJECT_CLASS (addrenum_class);
|
||
GSocketAddressEnumeratorClass *enumerator_class =
|
||
G_SOCKET_ADDRESS_ENUMERATOR_CLASS (addrenum_class);
|
||
|
||
enumerator_class->next = g_network_address_address_enumerator_next;
|
||
enumerator_class->next_async = g_network_address_address_enumerator_next_async;
|
||
enumerator_class->next_finish = g_network_address_address_enumerator_next_finish;
|
||
object_class->finalize = g_network_address_address_enumerator_finalize;
|
||
}
|
||
|
||
static GSocketAddressEnumerator *
|
||
g_network_address_connectable_enumerate (GSocketConnectable *connectable)
|
||
{
|
||
GNetworkAddressAddressEnumerator *addr_enum;
|
||
|
||
addr_enum = g_object_new (G_TYPE_NETWORK_ADDRESS_ADDRESS_ENUMERATOR, NULL);
|
||
addr_enum->addr = g_object_ref (G_NETWORK_ADDRESS (connectable));
|
||
|
||
return (GSocketAddressEnumerator *)addr_enum;
|
||
}
|
||
|
||
static GSocketAddressEnumerator *
|
||
g_network_address_connectable_proxy_enumerate (GSocketConnectable *connectable)
|
||
{
|
||
GNetworkAddress *self = G_NETWORK_ADDRESS (connectable);
|
||
GSocketAddressEnumerator *proxy_enum;
|
||
gchar *uri;
|
||
|
||
uri = g_uri_join (G_URI_FLAGS_NONE,
|
||
self->priv->scheme ? self->priv->scheme : "none",
|
||
NULL,
|
||
self->priv->hostname,
|
||
self->priv->port,
|
||
"",
|
||
NULL,
|
||
NULL);
|
||
|
||
proxy_enum = g_object_new (G_TYPE_PROXY_ADDRESS_ENUMERATOR,
|
||
"connectable", connectable,
|
||
"uri", uri,
|
||
NULL);
|
||
|
||
g_free (uri);
|
||
|
||
return proxy_enum;
|
||
}
|
||
|
||
static gchar *
|
||
g_network_address_connectable_to_string (GSocketConnectable *connectable)
|
||
{
|
||
GNetworkAddress *addr;
|
||
const gchar *scheme;
|
||
guint16 port;
|
||
GString *out; /* owned */
|
||
|
||
addr = G_NETWORK_ADDRESS (connectable);
|
||
out = g_string_new ("");
|
||
|
||
scheme = g_network_address_get_scheme (addr);
|
||
if (scheme != NULL)
|
||
g_string_append_printf (out, "%s:", scheme);
|
||
|
||
g_string_append (out, g_network_address_get_hostname (addr));
|
||
|
||
port = g_network_address_get_port (addr);
|
||
if (port != 0)
|
||
g_string_append_printf (out, ":%u", port);
|
||
|
||
return g_string_free (out, FALSE);
|
||
}
|