*
* You should have received a copy of the GNU Library 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.
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
*/
/**
* SECTION:gssdp-resource-browser
* @short_description: Class handling resource discovery.
*
- * #GUPnPResourceBrowser handles resource discovery. After creating a browser
+ * #GSSDPResourceBrowser handles resource discovery. After creating a browser
* and activating it, the ::resource-available and ::resource-unavailable
* signals will be emitted whenever the availability of a resource matching the
* specified discovery target changes. A discovery request is sent out
*/
#include <config.h>
-#include <libsoup/soup-date.h>
+#include <libsoup/soup.h>
#include <string.h>
#include <stdio.h>
+#include <stdlib.h>
#include "gssdp-resource-browser.h"
#include "gssdp-client-private.h"
#include "gssdp-protocol.h"
#include "gssdp-marshal.h"
+#define RESCAN_TIMEOUT 5 /* 5 seconds */
#define MAX_DISCOVERY_MESSAGES 3
#define DISCOVERY_FREQUENCY 500 /* 500 ms */
GSSDPClient *client;
char *target;
+ GRegex *target_regex;
gushort mx;
GHashTable *resources;
- guint timeout_id;
+ GSource *timeout_src;
guint num_discovery;
+ guint version;
+
+ GSource *refresh_cache_src;
+ GHashTable *fresh_resources;
};
enum {
typedef struct {
GSSDPResourceBrowser *resource_browser;
char *usn;
- guint timeout_id;
+ GSource *timeout_src;
} Resource;
/* Function prototypes */
const char *from_ip,
gushort from_port,
_GSSDPMessageType type,
- GHashTable *headers,
+ SoupMessageHeaders *headers,
gpointer user_data);
static void
resource_free (gpointer data);
start_discovery (GSSDPResourceBrowser *resource_browser);
static void
stop_discovery (GSSDPResourceBrowser *resource_browser);
+static gboolean
+refresh_cache (gpointer data);
static void
gssdp_resource_browser_init (GSSDPResourceBrowser *resource_browser)
resource_browser->priv->resources =
g_hash_table_new_full (g_str_hash,
g_str_equal,
- NULL,
+ g_free,
resource_free);
}
}
clear_cache (resource_browser);
+
+ G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->dispose (object);
}
static void
resource_browser = GSSDP_RESOURCE_BROWSER (object);
+ if (resource_browser->priv->target_regex)
+ g_regex_unref (resource_browser->priv->target_regex);
+
g_free (resource_browser->priv->target);
g_hash_table_destroy (resource_browser->priv->resources);
+
+ G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->finalize (object);
}
static void
{
GObjectClass *object_class;
- object_class = G_OBJECT_CLASS (klass);
+ object_class = G_OBJECT_CLASS (klass);
- object_class->set_property = gssdp_resource_browser_set_property;
- object_class->get_property = gssdp_resource_browser_get_property;
- object_class->dispose = gssdp_resource_browser_dispose;
- object_class->finalize = gssdp_resource_browser_finalize;
+ object_class->set_property = gssdp_resource_browser_set_property;
+ object_class->get_property = gssdp_resource_browser_get_property;
+ object_class->dispose = gssdp_resource_browser_dispose;
+ object_class->finalize = gssdp_resource_browser_finalize;
g_type_class_add_private (klass, sizeof (GSSDPResourceBrowserPrivate));
/**
- * GSSDPResourceBrowser:client
+ * GSSDPResourceBrowser:client:
*
* The #GSSDPClient to use.
**/
G_PARAM_STATIC_BLURB));
/**
- * GSSDPResourceBrowser:target
+ * GSSDPResourceBrowser:target:
*
* The discovery target.
**/
G_PARAM_STATIC_BLURB));
/**
- * GSSDPResourceBrowser:mx
+ * GSSDPResourceBrowser:mx:
*
* The maximum number of seconds in which to request other parties
* to respond.
G_PARAM_STATIC_BLURB));
/**
- * GSSDPResourceBrowser:active
+ * GSSDPResourceBrowser:active:
*
* Whether this browser is active or not.
**/
G_PARAM_STATIC_BLURB));
/**
- * GSSDPResourceBrowser::resource-available
+ * GSSDPResourceBrowser::resource-available:
* @resource_browser: The #GSSDPResourceBrowser that received the
* signal
* @usn: The USN of the discovered resource
- * @locations: A #GList of strings describing the locations of the
+ * @locations: (type GList*) (transfer none) (element-type utf8): A #GList of strings describing the locations of the
* discovered resource.
*
* The ::resource-available signal is emitted whenever a new resource
G_TYPE_POINTER);
/**
- * GSSDPResourceBrowser::resource-unavailable
+ * GSSDPResourceBrowser::resource-unavailable:
* @resource_browser: The #GSSDPResourceBrowser that received the
* signal
* @usn: The USN of the resource
}
/**
- * gssdp_resource_browser_new
+ * gssdp_resource_browser_new:
* @client: The #GSSDPClient to associate with
*
* Return value: A new #GSSDPResourceBrowser object.
NULL);
}
-/**
+/*
* Sets the #GSSDPClient @resource_browser is associated with to @client
- **/
+ */
static void
gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
GSSDPClient *client)
}
/**
- * gssdp_resource_browser_get_client
+ * gssdp_resource_browser_get_client:
* @resource_browser: A #GSSDPResourceBrowser
*
- * Return value: The #GSSDPClient @resource_browser is associated with.
+ * Returns: (transfer none): The #GSSDPClient @resource_browser is associated with.
**/
GSSDPClient *
gssdp_resource_browser_get_client (GSSDPResourceBrowser *resource_browser)
}
/**
- * gssdp_resource_browser_set_target
+ * gssdp_resource_browser_set_target:
* @resource_browser: A #GSSDPResourceBrowser
* @target: The browser target
*
gssdp_resource_browser_set_target (GSSDPResourceBrowser *resource_browser,
const char *target)
{
+ char *pattern;
+ char *version;
+ char *version_pattern;
+ GError *error;
+
g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
g_return_if_fail (target != NULL);
g_return_if_fail (!resource_browser->priv->active);
g_free (resource_browser->priv->target);
resource_browser->priv->target = g_strdup (target);
+ if (resource_browser->priv->target_regex)
+ g_regex_unref (resource_browser->priv->target_regex);
+
+ version_pattern = "([0-9]+)";
+ /* Make sure we have enough room for version pattern */
+ pattern = g_strndup (target,
+ strlen (target) + strlen (version_pattern));
+
+ version = g_strrstr (pattern, ":");
+ if (version != NULL &&
+ (g_strstr_len (pattern, -1, "uuid:") != pattern ||
+ version != g_strstr_len (pattern, -1, ":")) &&
+ g_regex_match_simple (version_pattern,
+ version + 1,
+ G_REGEX_ANCHORED,
+ G_REGEX_MATCH_ANCHORED)) {
+ resource_browser->priv->version = atoi (version + 1);
+ strcpy (version + 1, version_pattern);
+ }
+
+ error = NULL;
+ resource_browser->priv->target_regex = g_regex_new (pattern,
+ 0,
+ 0,
+ &error);
+ if (error) {
+ g_warning ("Error compiling regular expression '%s': %s",
+ pattern,
+ error->message);
+
+ g_error_free (error);
+ }
+
+ g_free (pattern);
g_object_notify (G_OBJECT (resource_browser), "target");
}
/**
- * gssdp_resource_browser_get_target
+ * gssdp_resource_browser_get_target:
* @resource_browser: A #GSSDPResourceBrowser
*
* Return value: The browser target.
}
/**
- * gssdp_resource_browser_set_mx
+ * gssdp_resource_browser_set_mx:
* @resource_browser: A #GSSDPResourceBrowser
* @mx: The to be used MX value
*
}
/**
- * gssdp_resource_browser_get_mx
+ * gssdp_resource_browser_get_mx:
* @resource_browser: A #GSSDPResourceBrowser
*
* Return value: The used MX value.
}
/**
- * gssdp_resource_browser_set_active
+ * gssdp_resource_browser_set_active:
* @resource_browser: A #GSSDPResourceBrowser
* @active: TRUE to activate @resource_browser
*
}
/**
- * gssdp_resource_browser_get_active
+ * gssdp_resource_browser_get_active:
* @resource_browser: A #GSSDPResourceBrowser
*
* Return value: TRUE if @resource_browser is active.
}
/**
- * Resource expired: Remove
+ * gssdp_resource_browser_rescan:
+ * @resource_browser: A #GSSDPResourceBrowser
+ *
+ * Begins discovery if @resource_browser is active and no discovery is
+ * performed. Otherwise does nothing.
+ *
+ * Return value: %TRUE if rescaning has been started.
**/
+gboolean
+gssdp_resource_browser_rescan (GSSDPResourceBrowser *resource_browser)
+{
+ g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
+
+ if (resource_browser->priv->active &&
+ resource_browser->priv->timeout_src == NULL &&
+ resource_browser->priv->refresh_cache_src == NULL) {
+ start_discovery (resource_browser);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/*
+ * Resource expired: Remove
+ */
static gboolean
resource_expire (gpointer user_data)
{
+ GSSDPResourceBrowser *resource_browser;
Resource *resource;
+ char *usn;
+ char *canonical_usn;
resource = user_data;
-
- g_signal_emit (resource->resource_browser,
- signals[RESOURCE_UNAVAILABLE],
- 0,
- resource->usn);
+ resource_browser = resource->resource_browser;
+
+ /* Steal the USN pointer from the resource as we need it for the signal
+ * emission.
+ */
+ usn = resource->usn;
+ resource->usn = NULL;
+
+ if (resource_browser->priv->version > 0) {
+ char *version;
+
+ version = g_strrstr (usn, ":");
+ canonical_usn = g_strndup (usn, version - usn);
+ } else {
+ canonical_usn = g_strdup (usn);
+ }
g_hash_table_remove (resource->resource_browser->priv->resources,
- resource->usn);
+ canonical_usn);
+
+ g_signal_emit (resource_browser,
+ signals[RESOURCE_UNAVAILABLE],
+ 0,
+ usn);
+ g_free (usn);
+ g_free (canonical_usn);
return FALSE;
}
static void
resource_available (GSSDPResourceBrowser *resource_browser,
- GHashTable *headers)
+ SoupMessageHeaders *headers)
{
- GSList *list;
const char *usn;
+ const char *header;
Resource *resource;
gboolean was_cached;
guint timeout;
GList *locations;
+ char *canonical_usn;
- list = g_hash_table_lookup (headers, "USN");
- if (!list)
+ usn = soup_message_headers_get_one (headers, "USN");
+ if (!usn)
return; /* No USN specified */
- usn = list->data;
+
+ if (resource_browser->priv->version > 0) {
+ char *version;
+
+ version = g_strrstr (usn, ":");
+ canonical_usn = g_strndup (usn, version - usn);
+ } else {
+ canonical_usn = g_strdup (usn);
+ }
/* Get from cache, if possible */
- resource = g_hash_table_lookup (resource_browser->priv->resources, usn);
+ resource = g_hash_table_lookup (resource_browser->priv->resources,
+ canonical_usn);
+ /* Put usn into fresh resources, so this resource will not be
+ * removed on cache refreshing. */
+ if (resource_browser->priv->fresh_resources != NULL) {
+ char *usn_copy = g_strdup (canonical_usn);
+
+ g_hash_table_insert (resource_browser->priv->fresh_resources,
+ usn_copy,
+ usn_copy);
+ }
+
if (resource) {
/* Remove old timeout */
- g_source_remove (resource->timeout_id);
+ g_source_destroy (resource->timeout_src);
was_cached = TRUE;
} else {
resource->usn = g_strdup (usn);
g_hash_table_insert (resource_browser->priv->resources,
- resource->usn,
+ canonical_usn,
resource);
was_cached = FALSE;
+
+ /* hash-table takes ownership of this */
+ canonical_usn = NULL;
}
+ if (canonical_usn != NULL)
+ g_free (canonical_usn);
+
/* Calculate new timeout */
- list = g_hash_table_lookup (headers, "Cache-Control");
- if (list) {
- GSList *l;
+ header = soup_message_headers_get_one (headers, "Cache-Control");
+ if (header) {
+ GSList *list;
int res;
res = 0;
- for (l = list; l; l = l->next) {
- res = sscanf (l->data,
+ for (list = soup_header_parse_list (header);
+ list;
+ list = list->next) {
+ res = sscanf (list->data,
"max-age = %d",
&timeout);
if (res == 1)
"default max-age of %d.\n"
"Header was:\n%s",
SSDP_DEFAULT_MAX_AGE,
- (char *) list->data);
+ header);
timeout = SSDP_DEFAULT_MAX_AGE;
}
+
+ soup_header_free_list (list);
} else {
- list = g_hash_table_lookup (headers, "Expires");
- if (list) {
+ const char *expires;
+
+ expires = soup_message_headers_get_one (headers, "Expires");
+ if (expires) {
+ SoupDate *soup_exp_time;
time_t exp_time, cur_time;
- exp_time = soup_date_parse (list->data);
+ soup_exp_time = soup_date_new_from_string (expires);
+ exp_time = soup_date_to_time_t (soup_exp_time);
+ soup_date_free (soup_exp_time);
+
cur_time = time (NULL);
if (exp_time > cur_time)
"default max-age of %d.\n"
"Header was:\n%s",
SSDP_DEFAULT_MAX_AGE,
- (char *) list->data);
+ expires);
timeout = SSDP_DEFAULT_MAX_AGE;
}
}
}
- resource->timeout_id = g_timeout_add (timeout * 1000,
- resource_expire,
- resource);
+ resource->timeout_src = g_timeout_source_new_seconds (timeout);
+ g_source_set_callback (resource->timeout_src,
+ resource_expire,
+ resource, NULL);
+
+ g_source_attach (resource->timeout_src,
+ g_main_context_get_thread_default ());
+
+ g_source_unref (resource->timeout_src);
/* Only continue with signal emission if this resource was not
* cached already */
/* Build list of locations */
locations = NULL;
- list = g_hash_table_lookup (headers, "Location");
- if (list)
- locations = g_list_append (locations, g_strdup (list->data));
+ header = soup_message_headers_get_one (headers, "Location");
+ if (header)
+ locations = g_list_append (locations, g_strdup (header));
- list = g_hash_table_lookup (headers, "AL");
- if (list) {
+ header = soup_message_headers_get_one (headers, "AL");
+ if (header) {
/* Parse AL header. The format is:
* <uri1><uri2>... */
- char *start, *end, *uri;
+ const char *start, *end;
+ char *uri;
- start = list->data;
+ start = header;
while ((start = strchr (start, '<'))) {
start += 1;
if (!start || !*start)
static void
resource_unavailable (GSSDPResourceBrowser *resource_browser,
- GHashTable *headers)
+ SoupMessageHeaders *headers)
{
- GSList *list;
const char *usn;
+ char *canonical_usn;
- list = g_hash_table_lookup (headers, "USN");
- if (!list)
+ usn = soup_message_headers_get_one (headers, "USN");
+ if (!usn)
return; /* No USN specified */
- usn = list->data;
+
+ if (resource_browser->priv->version > 0) {
+ char *version;
+
+ version = g_strrstr (usn, ":");
+ canonical_usn = g_strndup (usn, version - usn);
+ } else {
+ canonical_usn = g_strdup (usn);
+ }
/* Only process if we were cached */
- if (!g_hash_table_lookup (resource_browser->priv->resources, usn))
- return;
+ if (!g_hash_table_lookup (resource_browser->priv->resources,
+ canonical_usn))
+ goto out;
+
+ g_hash_table_remove (resource_browser->priv->resources,
+ canonical_usn);
g_signal_emit (resource_browser,
signals[RESOURCE_UNAVAILABLE],
0,
usn);
- g_hash_table_remove (resource_browser->priv->resources, usn);
+out:
+ g_free (canonical_usn);
+}
+
+static gboolean
+check_target_compat (GSSDPResourceBrowser *resource_browser,
+ const char *st)
+{
+ GMatchInfo *info;
+ int version;
+ char *tmp;
+
+ if (strcmp (resource_browser->priv->target,
+ GSSDP_ALL_RESOURCES) == 0)
+ return TRUE;
+
+ if (!g_regex_match (resource_browser->priv->target_regex,
+ st,
+ 0,
+ &info)) {
+ g_match_info_free (info);
+
+ return FALSE;
+ }
+
+ /* If there was no version to match, we're done */
+ if (resource_browser->priv->version == 0) {
+ g_match_info_free (info);
+
+ return TRUE;
+ }
+
+ if (g_match_info_get_match_count (info) != 2) {
+ g_match_info_free (info);
+
+ return FALSE;
+ }
+
+ version = atoi ((tmp = g_match_info_fetch (info, 1)));
+ g_free (tmp);
+ g_match_info_free (info);
+
+ return version >= resource_browser->priv->version;
}
static void
received_discovery_response (GSSDPResourceBrowser *resource_browser,
- GHashTable *headers)
+ SoupMessageHeaders *headers)
{
- GSList *list;
+ const char *st;
- list = g_hash_table_lookup (headers, "ST");
- if (!list)
+ st = soup_message_headers_get_one (headers, "ST");
+ if (!st)
return; /* No target specified */
- if (strcmp (resource_browser->priv->target, GSSDP_ALL_RESOURCES) != 0 &&
- strcmp (resource_browser->priv->target, list->data) != 0)
+ if (!check_target_compat (resource_browser, st))
return; /* Target doesn't match */
resource_available (resource_browser, headers);
static void
received_announcement (GSSDPResourceBrowser *resource_browser,
- GHashTable *headers)
+ SoupMessageHeaders *headers)
{
- GSList *list;
+ const char *header;
- list = g_hash_table_lookup (headers, "NT");
- if (!list)
+ header = soup_message_headers_get_one (headers, "NT");
+ if (!header)
return; /* No target specified */
- if (strcmp (resource_browser->priv->target, GSSDP_ALL_RESOURCES) != 0 &&
- strcmp (resource_browser->priv->target, list->data) != 0)
+ if (!check_target_compat (resource_browser, header))
return; /* Target doesn't match */
- list = g_hash_table_lookup (headers, "NTS");
- if (!list)
+ header = soup_message_headers_get_one (headers, "NTS");
+ if (!header)
return; /* No announcement type specified */
/* Check announcement type */
- if (strncmp (list->data,
+ if (strncmp (header,
SSDP_ALIVE_NTS,
strlen (SSDP_ALIVE_NTS)) == 0)
resource_available (resource_browser, headers);
- else if (strncmp (list->data,
+ else if (strncmp (header,
SSDP_BYEBYE_NTS,
strlen (SSDP_BYEBYE_NTS)) == 0)
resource_unavailable (resource_browser, headers);
}
-/**
+/*
* Received a message
- **/
+ */
static void
-message_received_cb (GSSDPClient *client,
- const char *from_ip,
- gushort from_port,
- _GSSDPMessageType type,
- GHashTable *headers,
- gpointer user_data)
+message_received_cb (GSSDPClient *client,
+ const char *from_ip,
+ gushort from_port,
+ _GSSDPMessageType type,
+ SoupMessageHeaders *headers,
+ gpointer user_data)
{
GSSDPResourceBrowser *resource_browser;
}
}
-/**
+/*
* Free a Resource structure and its contained data
- **/
+ */
static void
resource_free (gpointer data)
{
g_free (resource->usn);
- g_source_remove (resource->timeout_id);
+ g_source_destroy (resource->timeout_src);
g_slice_free (Resource, resource);
}
return TRUE;
}
-/**
+/*
* Clears the cached resources hash
- **/
+ */
static void
clear_cache (GSSDPResourceBrowser *resource_browser)
{
message = g_strdup_printf (SSDP_DISCOVERY_REQUEST,
resource_browser->priv->target,
- resource_browser->priv->mx);
+ resource_browser->priv->mx,
+ g_get_application_name () ?: "");
_gssdp_client_send_message (resource_browser->priv->client,
NULL,
0,
- message);
+ message,
+ _GSSDP_DISCOVERY_REQUEST);
g_free (message);
}
resource_browser->priv->num_discovery += 1;
if (resource_browser->priv->num_discovery >= MAX_DISCOVERY_MESSAGES) {
- resource_browser->priv->timeout_id = 0;
+ resource_browser->priv->timeout_src = NULL;
resource_browser->priv->num_discovery = 0;
+ /* Setup cache refreshing */
+ resource_browser->priv->refresh_cache_src =
+ g_timeout_source_new_seconds (RESCAN_TIMEOUT);
+ g_source_set_callback
+ (resource_browser->priv->refresh_cache_src,
+ refresh_cache,
+ resource_browser,
+ NULL);
+ g_source_attach (resource_browser->priv->refresh_cache_src,
+ g_main_context_get_thread_default ());
+ g_source_unref (resource_browser->priv->refresh_cache_src);
+
return FALSE;
} else
return TRUE;
/* And schedule the rest for later */
resource_browser->priv->num_discovery = 1;
- resource_browser->priv->timeout_id =
- g_timeout_add (DISCOVERY_FREQUENCY,
- discovery_timeout,
- resource_browser);
+ resource_browser->priv->timeout_src =
+ g_timeout_source_new (DISCOVERY_FREQUENCY);
+ g_source_set_callback (resource_browser->priv->timeout_src,
+ discovery_timeout,
+ resource_browser, NULL);
+
+ g_source_attach (resource_browser->priv->timeout_src,
+ g_main_context_get_thread_default ());
+
+ g_source_unref (resource_browser->priv->timeout_src);
+
+ /* Setup a set of responsive resources for cache refreshing */
+ resource_browser->priv->fresh_resources = g_hash_table_new_full
+ (g_str_hash,
+ g_str_equal,
+ g_free,
+ NULL);
}
/* Stops the sending of discovery messages */
static void
stop_discovery (GSSDPResourceBrowser *resource_browser)
{
- if (resource_browser->priv->timeout_id) {
- g_source_remove (resource_browser->priv->timeout_id);
- resource_browser->priv->timeout_id = 0;
+ if (resource_browser->priv->timeout_src) {
+ g_source_destroy (resource_browser->priv->timeout_src);
+ resource_browser->priv->timeout_src = NULL;
resource_browser->priv->num_discovery = 0;
}
+ if (resource_browser->priv->refresh_cache_src) {
+ g_source_destroy (resource_browser->priv->refresh_cache_src);
+ resource_browser->priv->refresh_cache_src = NULL;
+ }
+ if (resource_browser->priv->fresh_resources) {
+ g_hash_table_unref (resource_browser->priv->fresh_resources);
+ resource_browser->priv->fresh_resources = NULL;
+ }
+}
+
+static gboolean
+refresh_cache_helper (gpointer key, gpointer value, gpointer data)
+{
+ Resource *resource;
+ GHashTable *fresh_resources;
+
+ resource = value;
+ fresh_resources = data;
+
+ if (g_hash_table_lookup_extended (fresh_resources, key, NULL, NULL))
+ return FALSE;
+ else {
+ g_signal_emit (resource->resource_browser,
+ signals[RESOURCE_UNAVAILABLE],
+ 0,
+ resource->usn);
+
+ return TRUE;
+ }
+}
+
+/* Removes non-responsive resources */
+static gboolean
+refresh_cache (gpointer data)
+{
+ GSSDPResourceBrowser *resource_browser;
+
+ resource_browser = GSSDP_RESOURCE_BROWSER (data);
+ g_hash_table_foreach_remove (resource_browser->priv->resources,
+ refresh_cache_helper,
+ resource_browser->priv->fresh_resources);
+ g_hash_table_unref (resource_browser->priv->fresh_resources);
+ resource_browser->priv->fresh_resources = NULL;
+ resource_browser->priv->refresh_cache_src = NULL;
+
+ return FALSE;
}