2 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
4 * Author: Jorn Baayen <jorn@openedhand.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
16 * You should have received a copy of the GNU Library General Public
17 * License along with this library; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
23 * SECTION:gssdp-resource-browser
24 * @short_description: Class handling resource discovery.
26 * #GSSDPResourceBrowser handles resource discovery. After creating a browser
27 * and activating it, the ::resource-available and ::resource-unavailable
28 * signals will be emitted whenever the availability of a resource matching the
29 * specified discovery target changes. A discovery request is sent out
30 * automatically when activating the browser.
34 #include <libsoup/soup.h>
39 #include "gssdp-resource-browser.h"
40 #include "gssdp-client-private.h"
41 #include "gssdp-protocol.h"
42 #include "gssdp-marshal.h"
44 #define MAX_DISCOVERY_MESSAGES 3
45 #define DISCOVERY_FREQUENCY 500 /* 500 ms */
47 G_DEFINE_TYPE (GSSDPResourceBrowser,
48 gssdp_resource_browser,
51 struct _GSSDPResourceBrowserPrivate {
61 gulong message_received_id;
63 GHashTable *resources;
84 static guint signals[LAST_SIGNAL];
87 GSSDPResourceBrowser *resource_browser;
92 /* Function prototypes */
94 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
97 message_received_cb (GSSDPClient *client,
100 _GSSDPMessageType type,
101 SoupMessageHeaders *headers,
104 resource_free (gpointer data);
106 clear_cache (GSSDPResourceBrowser *resource_browser);
108 send_discovery_request (GSSDPResourceBrowser *resource_browser);
110 discovery_timeout (gpointer data);
112 start_discovery (GSSDPResourceBrowser *resource_browser);
114 stop_discovery (GSSDPResourceBrowser *resource_browser);
117 gssdp_resource_browser_init (GSSDPResourceBrowser *resource_browser)
119 resource_browser->priv = G_TYPE_INSTANCE_GET_PRIVATE
121 GSSDP_TYPE_RESOURCE_BROWSER,
122 GSSDPResourceBrowserPrivate);
124 resource_browser->priv->mx = SSDP_DEFAULT_MX;
126 resource_browser->priv->resources =
127 g_hash_table_new_full (g_str_hash,
134 gssdp_resource_browser_get_property (GObject *object,
139 GSSDPResourceBrowser *resource_browser;
141 resource_browser = GSSDP_RESOURCE_BROWSER (object);
143 switch (property_id) {
147 gssdp_resource_browser_get_client (resource_browser));
152 gssdp_resource_browser_get_target (resource_browser));
157 gssdp_resource_browser_get_mx (resource_browser));
162 gssdp_resource_browser_get_active (resource_browser));
165 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
171 gssdp_resource_browser_set_property (GObject *object,
176 GSSDPResourceBrowser *resource_browser;
178 resource_browser = GSSDP_RESOURCE_BROWSER (object);
180 switch (property_id) {
182 gssdp_resource_browser_set_client (resource_browser,
183 g_value_get_object (value));
186 gssdp_resource_browser_set_target (resource_browser,
187 g_value_get_string (value));
190 gssdp_resource_browser_set_mx (resource_browser,
191 g_value_get_uint (value));
194 gssdp_resource_browser_set_active (resource_browser,
195 g_value_get_boolean (value));
198 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
204 gssdp_resource_browser_dispose (GObject *object)
206 GSSDPResourceBrowser *resource_browser;
208 resource_browser = GSSDP_RESOURCE_BROWSER (object);
210 if (resource_browser->priv->client) {
211 if (g_signal_handler_is_connected
212 (resource_browser->priv->client,
213 resource_browser->priv->message_received_id)) {
214 g_signal_handler_disconnect
215 (resource_browser->priv->client,
216 resource_browser->priv->message_received_id);
219 stop_discovery (resource_browser);
221 g_object_unref (resource_browser->priv->client);
222 resource_browser->priv->client = NULL;
225 clear_cache (resource_browser);
227 G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->dispose (object);
231 gssdp_resource_browser_finalize (GObject *object)
233 GSSDPResourceBrowser *resource_browser;
235 resource_browser = GSSDP_RESOURCE_BROWSER (object);
237 if (resource_browser->priv->target_regex)
238 g_regex_unref (resource_browser->priv->target_regex);
240 g_free (resource_browser->priv->target);
242 g_hash_table_destroy (resource_browser->priv->resources);
244 G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->finalize (object);
248 gssdp_resource_browser_class_init (GSSDPResourceBrowserClass *klass)
250 GObjectClass *object_class;
252 object_class = G_OBJECT_CLASS (klass);
254 object_class->set_property = gssdp_resource_browser_set_property;
255 object_class->get_property = gssdp_resource_browser_get_property;
256 object_class->dispose = gssdp_resource_browser_dispose;
257 object_class->finalize = gssdp_resource_browser_finalize;
259 g_type_class_add_private (klass, sizeof (GSSDPResourceBrowserPrivate));
262 * GSSDPResourceBrowser:client:
264 * The #GSSDPClient to use.
266 g_object_class_install_property
272 "The associated client.",
274 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
275 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
276 G_PARAM_STATIC_BLURB));
279 * GSSDPResourceBrowser:target:
281 * The discovery target.
283 g_object_class_install_property
289 "The discovery target.",
292 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
293 G_PARAM_STATIC_BLURB));
296 * GSSDPResourceBrowser:mx:
298 * The maximum number of seconds in which to request other parties
301 g_object_class_install_property
307 "The maximum number of seconds in which to request "
308 "other parties to respond.",
313 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
314 G_PARAM_STATIC_BLURB));
317 * GSSDPResourceBrowser:active:
319 * Whether this browser is active or not.
321 g_object_class_install_property
327 "TRUE if the resource browser is active.",
330 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
331 G_PARAM_STATIC_BLURB));
334 * GSSDPResourceBrowser::resource-available:
335 * @resource_browser: The #GSSDPResourceBrowser that received the
337 * @usn: The USN of the discovered resource
338 * @locations: (type GList*) (transfer none) (element-type utf8): A #GList of strings describing the locations of the
339 * discovered resource.
341 * The ::resource-available signal is emitted whenever a new resource
342 * has become available.
344 signals[RESOURCE_AVAILABLE] =
345 g_signal_new ("resource-available",
346 GSSDP_TYPE_RESOURCE_BROWSER,
348 G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
351 gssdp_marshal_VOID__STRING_POINTER,
358 * GSSDPResourceBrowser::resource-unavailable:
359 * @resource_browser: The #GSSDPResourceBrowser that received the
361 * @usn: The USN of the resource
363 * The ::resource-unavailable signal is emitted whenever a resource
364 * is not available any more.
366 signals[RESOURCE_UNAVAILABLE] =
367 g_signal_new ("resource-unavailable",
368 GSSDP_TYPE_RESOURCE_BROWSER,
370 G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
371 resource_unavailable),
373 gssdp_marshal_VOID__STRING,
380 * gssdp_resource_browser_new:
381 * @client: The #GSSDPClient to associate with
383 * Return value: A new #GSSDPResourceBrowser object.
385 GSSDPResourceBrowser *
386 gssdp_resource_browser_new (GSSDPClient *client,
389 return g_object_new (GSSDP_TYPE_RESOURCE_BROWSER,
396 * Sets the #GSSDPClient @resource_browser is associated with to @client
399 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
402 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
403 g_return_if_fail (GSSDP_IS_CLIENT (client));
405 resource_browser->priv->client = g_object_ref (client);
407 resource_browser->priv->message_received_id =
408 g_signal_connect_object (resource_browser->priv->client,
410 G_CALLBACK (message_received_cb),
414 g_object_notify (G_OBJECT (resource_browser), "client");
418 * gssdp_resource_browser_get_client:
419 * @resource_browser: A #GSSDPResourceBrowser
421 * Returns: (transfer none): The #GSSDPClient @resource_browser is associated with.
424 gssdp_resource_browser_get_client (GSSDPResourceBrowser *resource_browser)
426 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
429 return resource_browser->priv->client;
433 * gssdp_resource_browser_set_target:
434 * @resource_browser: A #GSSDPResourceBrowser
435 * @target: The browser target
437 * Sets the browser target of @resource_browser to @target.
440 gssdp_resource_browser_set_target (GSSDPResourceBrowser *resource_browser,
445 char *version_pattern;
448 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
449 g_return_if_fail (target != NULL);
450 g_return_if_fail (!resource_browser->priv->active);
452 g_free (resource_browser->priv->target);
453 resource_browser->priv->target = g_strdup (target);
455 if (resource_browser->priv->target_regex)
456 g_regex_unref (resource_browser->priv->target_regex);
458 version_pattern = "([0-9]+)";
459 /* Make sure we have enough room for version pattern */
460 pattern = g_strndup (target,
461 strlen (target) + strlen (version_pattern));
463 version = g_strrstr (pattern, ":");
464 if (version != NULL &&
465 (g_strstr_len (pattern, -1, "uuid:") != pattern ||
466 version != g_strstr_len (pattern, -1, ":")) &&
467 g_regex_match_simple (version_pattern,
470 G_REGEX_MATCH_ANCHORED)) {
471 resource_browser->priv->version = atoi (version + 1);
472 strcpy (version + 1, version_pattern);
476 resource_browser->priv->target_regex = g_regex_new (pattern,
481 g_warning ("Error compiling regular expression '%s': %s",
485 g_error_free (error);
489 g_object_notify (G_OBJECT (resource_browser), "target");
493 * gssdp_resource_browser_get_target:
494 * @resource_browser: A #GSSDPResourceBrowser
496 * Return value: The browser target.
499 gssdp_resource_browser_get_target (GSSDPResourceBrowser *resource_browser)
501 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
504 return resource_browser->priv->target;
508 * gssdp_resource_browser_set_mx:
509 * @resource_browser: A #GSSDPResourceBrowser
510 * @mx: The to be used MX value
512 * Sets the used MX value of @resource_browser to @mx.
515 gssdp_resource_browser_set_mx (GSSDPResourceBrowser *resource_browser,
518 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
520 if (resource_browser->priv->mx == mx)
523 resource_browser->priv->mx = mx;
525 g_object_notify (G_OBJECT (resource_browser), "mx");
529 * gssdp_resource_browser_get_mx:
530 * @resource_browser: A #GSSDPResourceBrowser
532 * Return value: The used MX value.
535 gssdp_resource_browser_get_mx (GSSDPResourceBrowser *resource_browser)
537 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
539 return resource_browser->priv->mx;
543 * gssdp_resource_browser_set_active:
544 * @resource_browser: A #GSSDPResourceBrowser
545 * @active: TRUE to activate @resource_browser
547 * (De)activates @resource_browser.
550 gssdp_resource_browser_set_active (GSSDPResourceBrowser *resource_browser,
553 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
555 if (resource_browser->priv->active == active)
558 resource_browser->priv->active = active;
561 start_discovery (resource_browser);
563 stop_discovery (resource_browser);
565 clear_cache (resource_browser);
568 g_object_notify (G_OBJECT (resource_browser), "active");
572 * gssdp_resource_browser_get_active:
573 * @resource_browser: A #GSSDPResourceBrowser
575 * Return value: TRUE if @resource_browser is active.
578 gssdp_resource_browser_get_active (GSSDPResourceBrowser *resource_browser)
580 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
582 return resource_browser->priv->active;
586 * Resource expired: Remove
589 resource_expire (gpointer user_data)
591 GSSDPResourceBrowser *resource_browser;
596 resource = user_data;
597 resource_browser = resource->resource_browser;
599 /* Steal the USN pointer from the resource as we need it for the signal
603 resource->usn = NULL;
605 if (resource_browser->priv->version > 0) {
608 version = g_strrstr (usn, ":");
609 canonical_usn = g_strndup (usn, version - usn);
611 canonical_usn = g_strdup (usn);
614 g_hash_table_remove (resource->resource_browser->priv->resources,
617 g_signal_emit (resource_browser,
618 signals[RESOURCE_UNAVAILABLE],
622 g_free (canonical_usn);
628 resource_available (GSSDPResourceBrowser *resource_browser,
629 SoupMessageHeaders *headers)
639 usn = soup_message_headers_get_one (headers, "USN");
641 return; /* No USN specified */
643 if (resource_browser->priv->version > 0) {
646 version = g_strrstr (usn, ":");
647 canonical_usn = g_strndup (usn, version - usn);
649 canonical_usn = g_strdup (usn);
652 /* Get from cache, if possible */
653 resource = g_hash_table_lookup (resource_browser->priv->resources,
656 /* Remove old timeout */
657 g_source_destroy (resource->timeout_src);
661 /* Create new Resource data structure */
662 resource = g_slice_new (Resource);
664 resource->resource_browser = resource_browser;
665 resource->usn = g_strdup (usn);
667 g_hash_table_insert (resource_browser->priv->resources,
673 /* hash-table takes ownership of this */
674 canonical_usn = NULL;
677 if (canonical_usn != NULL)
678 g_free (canonical_usn);
680 /* Calculate new timeout */
681 header = soup_message_headers_get_one (headers, "Cache-Control");
688 for (list = soup_header_parse_list (header);
691 res = sscanf (list->data,
699 g_warning ("Invalid 'Cache-Control' header. Assuming "
700 "default max-age of %d.\n"
702 SSDP_DEFAULT_MAX_AGE,
705 timeout = SSDP_DEFAULT_MAX_AGE;
708 soup_header_free_list (list);
712 expires = soup_message_headers_get_one (headers, "Expires");
714 SoupDate *soup_exp_time;
715 time_t exp_time, cur_time;
717 soup_exp_time = soup_date_new_from_string (expires);
718 exp_time = soup_date_to_time_t (soup_exp_time);
719 soup_date_free (soup_exp_time);
721 cur_time = time (NULL);
723 if (exp_time > cur_time)
724 timeout = exp_time - cur_time;
726 g_warning ("Invalid 'Expires' header. Assuming "
727 "default max-age of %d.\n"
729 SSDP_DEFAULT_MAX_AGE,
732 timeout = SSDP_DEFAULT_MAX_AGE;
735 g_warning ("No 'Cache-Control' nor any 'Expires' "
736 "header was specified. Assuming default "
737 "max-age of %d.", SSDP_DEFAULT_MAX_AGE);
739 timeout = SSDP_DEFAULT_MAX_AGE;
743 resource->timeout_src = g_timeout_source_new_seconds (timeout);
744 g_source_set_callback (resource->timeout_src,
748 g_source_attach (resource->timeout_src,
749 g_main_context_get_thread_default ());
751 g_source_unref (resource->timeout_src);
753 /* Only continue with signal emission if this resource was not
758 /* Build list of locations */
761 header = soup_message_headers_get_one (headers, "Location");
763 locations = g_list_append (locations, g_strdup (header));
765 header = soup_message_headers_get_one (headers, "AL");
767 /* Parse AL header. The format is:
769 const char *start, *end;
773 while ((start = strchr (start, '<'))) {
775 if (!start || !*start)
778 end = strchr (start, '>');
782 uri = g_strndup (start, end - start);
783 locations = g_list_append (locations, uri);
790 g_signal_emit (resource_browser,
791 signals[RESOURCE_AVAILABLE],
798 g_free (locations->data);
800 locations = g_list_delete_link (locations, locations);
805 resource_unavailable (GSSDPResourceBrowser *resource_browser,
806 SoupMessageHeaders *headers)
811 usn = soup_message_headers_get_one (headers, "USN");
813 return; /* No USN specified */
815 if (resource_browser->priv->version > 0) {
818 version = g_strrstr (usn, ":");
819 canonical_usn = g_strndup (usn, version - usn);
821 canonical_usn = g_strdup (usn);
824 /* Only process if we were cached */
825 if (!g_hash_table_lookup (resource_browser->priv->resources,
829 g_hash_table_remove (resource_browser->priv->resources,
832 g_signal_emit (resource_browser,
833 signals[RESOURCE_UNAVAILABLE],
838 g_free (canonical_usn);
842 check_target_compat (GSSDPResourceBrowser *resource_browser,
849 if (strcmp (resource_browser->priv->target,
850 GSSDP_ALL_RESOURCES) == 0)
853 if (!g_regex_match (resource_browser->priv->target_regex,
857 g_match_info_free (info);
862 /* If there was no version to match, we're done */
863 if (resource_browser->priv->version == 0) {
864 g_match_info_free (info);
869 if (g_match_info_get_match_count (info) != 2) {
870 g_match_info_free (info);
875 version = atoi ((tmp = g_match_info_fetch (info, 1)));
877 g_match_info_free (info);
879 return version >= resource_browser->priv->version;
883 received_discovery_response (GSSDPResourceBrowser *resource_browser,
884 SoupMessageHeaders *headers)
888 st = soup_message_headers_get_one (headers, "ST");
890 return; /* No target specified */
892 if (!check_target_compat (resource_browser, st))
893 return; /* Target doesn't match */
895 resource_available (resource_browser, headers);
899 received_announcement (GSSDPResourceBrowser *resource_browser,
900 SoupMessageHeaders *headers)
904 header = soup_message_headers_get_one (headers, "NT");
906 return; /* No target specified */
908 if (!check_target_compat (resource_browser, header))
909 return; /* Target doesn't match */
911 header = soup_message_headers_get_one (headers, "NTS");
913 return; /* No announcement type specified */
915 /* Check announcement type */
918 strlen (SSDP_ALIVE_NTS)) == 0)
919 resource_available (resource_browser, headers);
920 else if (strncmp (header,
922 strlen (SSDP_BYEBYE_NTS)) == 0)
923 resource_unavailable (resource_browser, headers);
930 message_received_cb (GSSDPClient *client,
933 _GSSDPMessageType type,
934 SoupMessageHeaders *headers,
937 GSSDPResourceBrowser *resource_browser;
939 resource_browser = GSSDP_RESOURCE_BROWSER (user_data);
941 if (!resource_browser->priv->active)
945 case _GSSDP_DISCOVERY_RESPONSE:
946 received_discovery_response (resource_browser, headers);
948 case _GSSDP_ANNOUNCEMENT:
949 received_announcement (resource_browser, headers);
957 * Free a Resource structure and its contained data
960 resource_free (gpointer data)
966 g_free (resource->usn);
968 g_source_destroy (resource->timeout_src);
970 g_slice_free (Resource, resource);
974 clear_cache_helper (gpointer key, gpointer value, gpointer data)
980 g_signal_emit (resource->resource_browser,
981 signals[RESOURCE_UNAVAILABLE],
989 * Clears the cached resources hash
992 clear_cache (GSSDPResourceBrowser *resource_browser)
995 g_hash_table_foreach_remove (resource_browser->priv->resources,
1000 /* Sends discovery request */
1002 send_discovery_request (GSSDPResourceBrowser *resource_browser)
1006 message = g_strdup_printf (SSDP_DISCOVERY_REQUEST,
1007 resource_browser->priv->target,
1008 resource_browser->priv->mx,
1009 g_get_application_name () ?: "");
1011 _gssdp_client_send_message (resource_browser->priv->client,
1015 _GSSDP_DISCOVERY_REQUEST);
1021 discovery_timeout (gpointer data)
1023 GSSDPResourceBrowser *resource_browser;
1025 resource_browser = GSSDP_RESOURCE_BROWSER (data);
1027 send_discovery_request (resource_browser);
1029 resource_browser->priv->num_discovery += 1;
1031 if (resource_browser->priv->num_discovery >= MAX_DISCOVERY_MESSAGES) {
1032 resource_browser->priv->timeout_src = NULL;
1033 resource_browser->priv->num_discovery = 0;
1040 /* Starts sending discovery requests */
1042 start_discovery (GSSDPResourceBrowser *resource_browser)
1045 send_discovery_request (resource_browser);
1047 /* And schedule the rest for later */
1048 resource_browser->priv->num_discovery = 1;
1049 resource_browser->priv->timeout_src =
1050 g_timeout_source_new (DISCOVERY_FREQUENCY);
1051 g_source_set_callback (resource_browser->priv->timeout_src,
1053 resource_browser, NULL);
1055 g_source_attach (resource_browser->priv->timeout_src,
1056 g_main_context_get_thread_default ());
1058 g_source_unref (resource_browser->priv->timeout_src);
1061 /* Stops the sending of discovery messages */
1063 stop_discovery (GSSDPResourceBrowser *resource_browser)
1065 if (resource_browser->priv->timeout_src) {
1066 g_source_destroy (resource_browser->priv->timeout_src);
1067 resource_browser->priv->timeout_src = NULL;
1068 resource_browser->priv->num_discovery = 0;