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-group
24 * @short_description: Class for controlling resource announcement.
26 * A #GSSDPResourceGroup is a group of SSDP resources whose availability can
27 * be controlled as one. This is useful when one needs to announce a single
28 * service as multiple SSDP resources (UPnP does this for example).
36 #include <libsoup/soup.h>
38 #include "gssdp-resource-group.h"
39 #include "gssdp-resource-browser.h"
40 #include "gssdp-client-private.h"
41 #include "gssdp-protocol.h"
43 G_DEFINE_TYPE (GSSDPResourceGroup,
47 #define DEFAULT_MAN_HEADER "\"ssdp:discover\""
49 struct _GSSDPResourceGroupPrivate {
58 gulong message_received_id;
62 guint last_resource_id;
65 GQueue *message_queue;
78 GSSDPResourceGroup *resource_group;
91 gboolean initial_byebye_sent;
100 GSource *timeout_src;
103 #define DEFAULT_MESSAGE_DELAY 120
104 #define DEFAULT_ANNOUNCEMENT_SET_SIZE 3
105 #define VERSION_PATTERN "[0-9]+$"
107 /* Function prototypes */
109 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
110 GSSDPClient *client);
112 resource_group_timeout (gpointer user_data);
114 message_received_cb (GSSDPClient *client,
117 _GSSDPMessageType type,
118 SoupMessageHeaders *headers,
121 resource_alive (Resource *resource);
123 resource_byebye (Resource *resource);
125 resource_free (Resource *resource);
127 discovery_response_timeout (gpointer user_data);
129 discovery_response_free (DiscoveryResponse *response);
131 process_queue (gpointer data);
133 get_version_for_target (char *target);
135 create_target_regex (const char *target,
139 send_initial_resource_byebye (Resource *resource);
142 gssdp_resource_group_init (GSSDPResourceGroup *resource_group)
144 resource_group->priv = G_TYPE_INSTANCE_GET_PRIVATE
146 GSSDP_TYPE_RESOURCE_GROUP,
147 GSSDPResourceGroupPrivate);
149 resource_group->priv->max_age = SSDP_DEFAULT_MAX_AGE;
150 resource_group->priv->message_delay = DEFAULT_MESSAGE_DELAY;
152 resource_group->priv->message_queue = g_queue_new ();
156 gssdp_resource_group_get_property (GObject *object,
161 GSSDPResourceGroup *resource_group;
163 resource_group = GSSDP_RESOURCE_GROUP (object);
165 switch (property_id) {
169 gssdp_resource_group_get_client (resource_group));
174 gssdp_resource_group_get_max_age (resource_group));
179 gssdp_resource_group_get_available (resource_group));
181 case PROP_MESSAGE_DELAY:
184 gssdp_resource_group_get_message_delay
188 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
194 gssdp_resource_group_set_property (GObject *object,
199 GSSDPResourceGroup *resource_group;
201 resource_group = GSSDP_RESOURCE_GROUP (object);
203 switch (property_id) {
205 gssdp_resource_group_set_client (resource_group,
206 g_value_get_object (value));
209 gssdp_resource_group_set_max_age (resource_group,
210 g_value_get_long (value));
213 gssdp_resource_group_set_available
214 (resource_group, g_value_get_boolean (value));
216 case PROP_MESSAGE_DELAY:
217 gssdp_resource_group_set_message_delay
218 (resource_group, g_value_get_uint (value));
221 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
227 gssdp_resource_group_dispose (GObject *object)
229 GSSDPResourceGroup *resource_group;
230 GSSDPResourceGroupPrivate *priv;
232 resource_group = GSSDP_RESOURCE_GROUP (object);
233 priv = resource_group->priv;
235 while (priv->resources) {
236 resource_free (priv->resources->data);
238 g_list_delete_link (priv->resources,
242 if (priv->message_queue) {
243 /* send messages without usual delay */
244 while (!g_queue_is_empty (priv->message_queue)) {
246 process_queue (resource_group);
248 g_free (g_queue_pop_head
249 (priv->message_queue));
252 g_queue_free (priv->message_queue);
253 priv->message_queue = NULL;
256 if (priv->message_src) {
257 g_source_destroy (priv->message_src);
258 priv->message_src = NULL;
261 if (priv->timeout_src) {
262 g_source_destroy (priv->timeout_src);
263 priv->timeout_src = NULL;
267 if (g_signal_handler_is_connected
269 priv->message_received_id)) {
270 g_signal_handler_disconnect
272 priv->message_received_id);
275 g_object_unref (priv->client);
279 G_OBJECT_CLASS (gssdp_resource_group_parent_class)->dispose (object);
283 gssdp_resource_group_class_init (GSSDPResourceGroupClass *klass)
285 GObjectClass *object_class;
287 object_class = G_OBJECT_CLASS (klass);
289 object_class->set_property = gssdp_resource_group_set_property;
290 object_class->get_property = gssdp_resource_group_get_property;
291 object_class->dispose = gssdp_resource_group_dispose;
293 g_type_class_add_private (klass, sizeof (GSSDPResourceGroupPrivate));
296 * GSSDPResourceGroup:client:
298 * The #GSSDPClient to use.
300 g_object_class_install_property
306 "The associated client.",
308 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
309 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
310 G_PARAM_STATIC_BLURB));
313 * GSSDPResourceGroup:max-age:
315 * The number of seconds our advertisements are valid.
317 g_object_class_install_property
323 "The number of seconds advertisements are valid.",
326 SSDP_DEFAULT_MAX_AGE,
328 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
329 G_PARAM_STATIC_BLURB));
332 * GSSDPResourceGroup:available:
334 * Whether this group of resources is available or not.
336 g_object_class_install_property
342 "Whether this group of resources is available or "
346 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
347 G_PARAM_STATIC_BLURB));
350 * GSSDPResourceGroup:message-delay:
352 * The minimum number of milliseconds between SSDP messages.
353 * The default is 120 based on DLNA specification.
355 g_object_class_install_property
361 "The minimum number of milliseconds between SSDP "
365 DEFAULT_MESSAGE_DELAY,
367 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
368 G_PARAM_STATIC_BLURB));
372 * gssdp_resource_group_new:
373 * @client: The #GSSDPClient to associate with
375 * Return value: A new #GSSDPResourceGroup object.
378 gssdp_resource_group_new (GSSDPClient *client)
380 return g_object_new (GSSDP_TYPE_RESOURCE_GROUP,
386 * Sets the #GSSDPClient @resource_group is associated with @client
389 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
392 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
393 g_return_if_fail (GSSDP_IS_CLIENT (client));
395 resource_group->priv->client = g_object_ref (client);
397 resource_group->priv->message_received_id =
398 g_signal_connect_object (resource_group->priv->client,
400 G_CALLBACK (message_received_cb),
404 g_object_notify (G_OBJECT (resource_group), "client");
408 * gssdp_resource_group_get_client:
409 * @resource_group: A #GSSDPResourceGroup
411 * Returns: (transfer none): The #GSSDPClient @resource_group is associated with.
414 gssdp_resource_group_get_client (GSSDPResourceGroup *resource_group)
416 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), NULL);
418 return resource_group->priv->client;
422 * gssdp_resource_group_set_max_age:
423 * @resource_group: A #GSSDPResourceGroup
424 * @max_age: The number of seconds advertisements are valid
426 * Sets the number of seconds advertisements are valid to @max_age.
429 gssdp_resource_group_set_max_age (GSSDPResourceGroup *resource_group,
432 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
434 if (resource_group->priv->max_age == max_age)
437 resource_group->priv->max_age = max_age;
439 g_object_notify (G_OBJECT (resource_group), "max-age");
443 * gssdp_resource_group_get_max_age:
444 * @resource_group: A #GSSDPResourceGroup
446 * Return value: The number of seconds advertisements are valid.
449 gssdp_resource_group_get_max_age (GSSDPResourceGroup *resource_group)
451 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
453 return resource_group->priv->max_age;
457 * gssdp_resource_group_set_message_delay:
458 * @resource_group: A #GSSDPResourceGroup
459 * @message_delay: The message delay in ms.
461 * Sets the minimum time between each SSDP message.
464 gssdp_resource_group_set_message_delay (GSSDPResourceGroup *resource_group,
467 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
469 if (resource_group->priv->message_delay == message_delay)
472 resource_group->priv->message_delay = message_delay;
474 g_object_notify (G_OBJECT (resource_group), "message-delay");
478 * gssdp_resource_group_get_message_delay:
479 * @resource_group: A #GSSDPResourceGroup
481 * Return value: the minimum time between each SSDP message in ms.
484 gssdp_resource_group_get_message_delay (GSSDPResourceGroup *resource_group)
486 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
488 return resource_group->priv->message_delay;
492 send_initial_resource_byebye (Resource *resource)
494 if (!resource->initial_byebye_sent) {
495 /* Unannounce before first announce. This is
496 done to minimize the possibility of
497 control points thinking that this is just
499 resource_byebye (resource);
501 resource->initial_byebye_sent = TRUE;
506 send_announcement_set (GList *resources, GFunc message_function)
510 for (i = 0; i < DEFAULT_ANNOUNCEMENT_SET_SIZE; i++) {
511 g_list_foreach (resources, message_function, NULL);
516 * gssdp_resource_group_set_available:
517 * @resource_group: A #GSSDPResourceGroup
518 * @available: TRUE if @resource_group should be available (advertised)
520 * Sets @resource_group<!-- -->s availability to @available. Changing
521 * @resource_group<!-- -->s availability causes it to announce its new state
522 * to listening SSDP clients.
525 gssdp_resource_group_set_available (GSSDPResourceGroup *resource_group,
528 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
530 if (resource_group->priv->available == available)
533 resource_group->priv->available = available;
538 /* We want to re-announce at least 3 times before the resource
539 * group expires to cope with the unrelialble nature of UDP.
541 * Read the paragraphs about 'CACHE-CONTROL' on pages 21-22 of
542 * UPnP Device Architecture Document v1.1 for further details.
544 timeout = resource_group->priv->max_age;
545 if (G_LIKELY (timeout > 6))
546 timeout = (timeout / 3) - 1;
548 /* Add re-announcement timer */
549 resource_group->priv->timeout_src =
550 g_timeout_source_new_seconds (timeout);
551 g_source_set_callback (resource_group->priv->timeout_src,
552 resource_group_timeout,
553 resource_group, NULL);
555 g_source_attach (resource_group->priv->timeout_src,
556 g_main_context_get_thread_default ());
558 g_source_unref (resource_group->priv->timeout_src);
560 /* Make sure initial byebyes are sent grouped before initial
562 send_announcement_set (resource_group->priv->resources,
563 (GFunc) send_initial_resource_byebye);
565 send_announcement_set (resource_group->priv->resources,
566 (GFunc) resource_alive);
568 /* Unannounce all resources */
569 send_announcement_set (resource_group->priv->resources,
570 (GFunc) resource_byebye);
572 /* Remove re-announcement timer */
573 g_source_destroy (resource_group->priv->timeout_src);
574 resource_group->priv->timeout_src = NULL;
577 g_object_notify (G_OBJECT (resource_group), "available");
581 * gssdp_resource_group_get_available:
582 * @resource_group: A #GSSDPResourceGroup
584 * Return value: TRUE if @resource_group is available (advertised).
587 gssdp_resource_group_get_available (GSSDPResourceGroup *resource_group)
589 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), FALSE);
591 return resource_group->priv->available;
595 * gssdp_resource_group_add_resource:
596 * @resource_group: An @GSSDPResourceGroup
597 * @target: The resource's target
598 * @usn: The resource's USN
599 * @locations: (element-type utf8): A #GList of the resource's locations
601 * Adds a resource with target @target, USN @usn, and locations @locations
602 * to @resource_group.
604 * Return value: The ID of the added resource.
607 gssdp_resource_group_add_resource (GSSDPResourceGroup *resource_group,
616 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
617 g_return_val_if_fail (target != NULL, 0);
618 g_return_val_if_fail (usn != NULL, 0);
619 g_return_val_if_fail (locations != NULL, 0);
621 resource = g_slice_new0 (Resource);
623 resource->resource_group = resource_group;
625 resource->target = g_strdup (target);
626 resource->usn = g_strdup (usn);
629 resource->target_regex = create_target_regex (target, &resource->version, &error);
631 g_warning ("Error compiling regular expression for '%s': %s",
635 g_error_free (error);
636 resource_free (resource);
641 resource->initial_byebye_sent = FALSE;
643 for (l = locations; l; l = l->next) {
644 resource->locations = g_list_append (resource->locations,
648 resource_group->priv->resources =
649 g_list_prepend (resource_group->priv->resources, resource);
651 resource->id = ++resource_group->priv->last_resource_id;
653 if (resource_group->priv->available)
654 resource_alive (resource);
660 * gssdp_resource_group_add_resource_simple:
661 * @resource_group: An @GSSDPResourceGroup
662 * @target: The resource's target
663 * @usn: The resource's USN
664 * @location: The resource's location
666 * Adds a resource with target @target, USN @usn, and location @location
667 * to @resource_group.
669 * Return value: The ID of the added resource.
672 gssdp_resource_group_add_resource_simple (GSSDPResourceGroup *resource_group,
675 const char *location)
677 GList *locations = NULL;
680 locations = g_list_append (locations, (gpointer) location);
681 resource_id = gssdp_resource_group_add_resource (resource_group, target, usn, locations);
683 g_list_free (locations);
689 * gssdp_resource_group_remove_resource:
690 * @resource_group: An @GSSDPResourceGroup
691 * @resource_id: The ID of the resource to remove
693 * Removes the resource with ID @resource_id from @resource_group.
696 gssdp_resource_group_remove_resource (GSSDPResourceGroup *resource_group,
701 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
702 g_return_if_fail (resource_id > 0);
704 for (l = resource_group->priv->resources; l; l = l->next) {
709 if (resource->id == resource_id) {
710 resource_group->priv->resources =
711 g_list_remove (resource_group->priv->resources,
714 resource_free (resource);
722 * Called to re-announce all resources periodically
725 resource_group_timeout (gpointer user_data)
727 GSSDPResourceGroup *resource_group;
729 resource_group = GSSDP_RESOURCE_GROUP (user_data);
731 send_announcement_set (resource_group->priv->resources,
732 (GFunc) resource_alive);
741 message_received_cb (G_GNUC_UNUSED GSSDPClient *client,
744 _GSSDPMessageType type,
745 SoupMessageHeaders *headers,
748 GSSDPResourceGroup *resource_group;
749 const char *target, *mx_str, *version_str, *man;
754 resource_group = GSSDP_RESOURCE_GROUP (user_data);
756 /* Only process if we are available */
757 if (!resource_group->priv->available)
760 /* We only handle discovery requests */
761 if (type != _GSSDP_DISCOVERY_REQUEST)
765 target = soup_message_headers_get_one (headers, "ST");
767 g_warning ("Discovery request did not have an ST header");
772 /* Is this the "ssdp:all" target? */
773 want_all = (strcmp (target, GSSDP_ALL_RESOURCES) == 0);
776 mx_str = soup_message_headers_get_one (headers, "MX");
777 if (!mx_str || atoi (mx_str) <= 0) {
778 g_warning ("Discovery request did not have a valid MX header");
783 man = soup_message_headers_get_one (headers, "MAN");
784 if (!man || strcmp (man, DEFAULT_MAN_HEADER) != 0) {
785 g_warning ("Discovery request did not have a valid MAN header");
792 /* Extract version */
793 version_str = get_version_for_target ((char *) target);
794 if (version_str != NULL)
795 version = atoi (version_str);
799 /* Find matching resource */
800 for (l = resource_group->priv->resources; l; l = l->next) {
806 (g_regex_match (resource->target_regex,
810 (guint) version <= resource->version)) {
813 DiscoveryResponse *response;
815 /* Get a random timeout from the interval [0, mx] */
816 timeout = g_random_int_range (0, mx * 1000);
818 /* Prepare response */
819 response = g_slice_new (DiscoveryResponse);
821 response->dest_ip = g_strdup (from_ip);
822 response->dest_port = from_port;
823 response->resource = resource;
826 response->target = g_strdup (resource->target);
828 response->target = g_strdup (target);
831 response->timeout_src = g_timeout_source_new (timeout);
832 g_source_set_callback (response->timeout_src,
833 discovery_response_timeout,
836 g_source_attach (response->timeout_src,
837 g_main_context_get_thread_default ());
839 g_source_unref (response->timeout_src);
841 /* Add to resource */
842 resource->responses =
843 g_list_prepend (resource->responses, response);
849 * Construct the AL (Alternative Locations) header for @resource
852 construct_al (Resource *resource)
854 if (resource->locations->next) {
858 al_string = g_string_new ("AL: ");
860 for (l = resource->locations->next; l; l = l->next) {
861 g_string_append_c (al_string, '<');
862 g_string_append (al_string, l->data);
863 g_string_append_c (al_string, '>');
866 g_string_append (al_string, "\r\n");
868 return g_string_free (al_string, FALSE);
874 construct_usn (const char *usn,
875 const char *response_target,
876 const char *resource_target)
882 needle = strstr (usn, resource_target);
884 return g_strdup (usn);
886 prefix = g_strndup (usn, needle - usn);
887 st = g_strconcat (prefix, response_target, NULL);
895 * Send a discovery response
898 discovery_response_timeout (gpointer user_data)
900 DiscoveryResponse *response;
903 char *al, *date_str, *message;
907 response = user_data;
910 client = response->resource->resource_group->priv->client;
912 max_age = response->resource->resource_group->priv->max_age;
914 al = construct_al (response->resource);
915 usn = construct_usn (response->resource->usn,
917 response->resource->target);
918 date = soup_date_new_from_now (0);
919 date_str = soup_date_to_string (date, SOUP_DATE_HTTP);
920 soup_date_free (date);
922 message = g_strdup_printf (SSDP_DISCOVERY_RESPONSE,
923 (char *) response->resource->locations->data,
926 gssdp_client_get_server_id (client),
931 _gssdp_client_send_message (client,
935 _GSSDP_DISCOVERY_RESPONSE);
942 discovery_response_free (response);
948 * Free a DiscoveryResponse structure and its contained data
951 discovery_response_free (DiscoveryResponse *response)
953 response->resource->responses =
954 g_list_remove (response->resource->responses, response);
956 g_source_destroy (response->timeout_src);
958 g_free (response->dest_ip);
959 g_free (response->target);
961 g_slice_free (DiscoveryResponse, response);
965 * Send the next queued message, if any
968 process_queue (gpointer data)
970 GSSDPResourceGroup *resource_group;
972 resource_group = GSSDP_RESOURCE_GROUP (data);
974 if (g_queue_is_empty (resource_group->priv->message_queue)) {
975 /* this is the timeout after last message in queue */
976 resource_group->priv->message_src = NULL;
983 client = resource_group->priv->client;
984 message = g_queue_pop_head
985 (resource_group->priv->message_queue);
987 _gssdp_client_send_message (client,
991 _GSSDP_DISCOVERY_RESPONSE);
999 * Add a message to sending queue
1001 * Do not free @message.
1004 queue_message (GSSDPResourceGroup *resource_group,
1007 g_queue_push_tail (resource_group->priv->message_queue,
1010 if (resource_group->priv->message_src == NULL) {
1011 /* nothing in the queue: process message immediately
1012 and add a timeout for (possible) next message */
1013 process_queue (resource_group);
1014 resource_group->priv->message_src = g_timeout_source_new (
1015 resource_group->priv->message_delay);
1016 g_source_set_callback (resource_group->priv->message_src,
1017 process_queue, resource_group, NULL);
1018 g_source_attach (resource_group->priv->message_src,
1019 g_main_context_get_thread_default ());
1020 g_source_unref (resource_group->priv->message_src);
1025 * Send ssdp:alive message for @resource
1028 resource_alive (Resource *resource)
1030 GSSDPClient *client;
1034 /* Send initial byebye if not sent already */
1035 send_initial_resource_byebye (resource);
1038 client = resource->resource_group->priv->client;
1040 max_age = resource->resource_group->priv->max_age;
1042 al = construct_al (resource);
1044 message = g_strdup_printf (SSDP_ALIVE_MESSAGE,
1046 (char *) resource->locations->data,
1048 gssdp_client_get_server_id (client),
1052 queue_message (resource->resource_group, message);
1058 * Send ssdp:byebye message for @resource
1061 resource_byebye (Resource *resource)
1066 message = g_strdup_printf (SSDP_BYEBYE_MESSAGE,
1070 queue_message (resource->resource_group, message);
1074 * Free a Resource structure and its contained data
1077 resource_free (Resource *resource)
1079 while (resource->responses)
1080 discovery_response_free (resource->responses->data);
1082 if (resource->resource_group->priv->available)
1083 resource_byebye (resource);
1085 g_free (resource->usn);
1086 g_free (resource->target);
1088 if (resource->target_regex)
1089 g_regex_unref (resource->target_regex);
1091 while (resource->locations) {
1092 g_free (resource->locations->data);
1093 resource->locations = g_list_delete_link (resource->locations,
1094 resource->locations);
1097 g_slice_free (Resource, resource);
1100 /* Gets you the pointer to the version part in the target string */
1102 get_version_for_target (char *target)
1106 if (strncmp (target, "urn:", 4) != 0) {
1107 /* target is not a URN so no version. */
1111 version = g_strrstr (target, ":") + 1;
1112 if (version == NULL ||
1113 !g_regex_match_simple (VERSION_PATTERN, version, 0, 0))
1120 create_target_regex (const char *target, guint *version, GError **error)
1127 /* Make sure we have enough room for version pattern */
1128 pattern = g_strndup (target,
1129 strlen (target) + strlen (VERSION_PATTERN));
1131 version_str = get_version_for_target (pattern);
1132 if (version_str != NULL) {
1133 *version = atoi (version_str);
1134 strcpy (version_str, VERSION_PATTERN);
1137 regex = g_regex_new (pattern, 0, 0, error);