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 struct _GSSDPResourceGroupPrivate {
56 gulong message_received_id;
60 guint last_resource_id;
63 GQueue *message_queue;
76 GSSDPResourceGroup *resource_group;
89 gboolean initial_byebye_sent;
101 #define DEFAULT_MESSAGE_DELAY 120
102 #define DEFAULT_ANNOUNCEMENT_SET_SIZE 3
103 #define VERSION_PATTERN "[0-9]+$"
105 /* Function prototypes */
107 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
108 GSSDPClient *client);
110 resource_group_timeout (gpointer user_data);
112 message_received_cb (GSSDPClient *client,
115 _GSSDPMessageType type,
116 SoupMessageHeaders *headers,
119 resource_alive (Resource *resource);
121 resource_byebye (Resource *resource);
123 resource_free (Resource *resource);
125 discovery_response_timeout (gpointer user_data);
127 discovery_response_free (DiscoveryResponse *response);
129 process_queue (gpointer data);
131 get_version_for_target (char *target);
133 create_target_regex (const char *target,
137 send_initial_resource_byebye (Resource *resource);
140 gssdp_resource_group_init (GSSDPResourceGroup *resource_group)
142 resource_group->priv = G_TYPE_INSTANCE_GET_PRIVATE
144 GSSDP_TYPE_RESOURCE_GROUP,
145 GSSDPResourceGroupPrivate);
147 resource_group->priv->max_age = SSDP_DEFAULT_MAX_AGE;
148 resource_group->priv->message_delay = DEFAULT_MESSAGE_DELAY;
150 resource_group->priv->message_queue = g_queue_new ();
154 gssdp_resource_group_get_property (GObject *object,
159 GSSDPResourceGroup *resource_group;
161 resource_group = GSSDP_RESOURCE_GROUP (object);
163 switch (property_id) {
167 gssdp_resource_group_get_client (resource_group));
172 gssdp_resource_group_get_max_age (resource_group));
177 gssdp_resource_group_get_available (resource_group));
179 case PROP_MESSAGE_DELAY:
182 gssdp_resource_group_get_message_delay
186 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
192 gssdp_resource_group_set_property (GObject *object,
197 GSSDPResourceGroup *resource_group;
199 resource_group = GSSDP_RESOURCE_GROUP (object);
201 switch (property_id) {
203 gssdp_resource_group_set_client (resource_group,
204 g_value_get_object (value));
207 gssdp_resource_group_set_max_age (resource_group,
208 g_value_get_long (value));
211 gssdp_resource_group_set_available
212 (resource_group, g_value_get_boolean (value));
214 case PROP_MESSAGE_DELAY:
215 gssdp_resource_group_set_message_delay
216 (resource_group, g_value_get_uint (value));
219 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
225 gssdp_resource_group_dispose (GObject *object)
227 GSSDPResourceGroup *resource_group;
228 GSSDPResourceGroupPrivate *priv;
230 resource_group = GSSDP_RESOURCE_GROUP (object);
231 priv = resource_group->priv;
233 while (priv->resources) {
234 resource_free (priv->resources->data);
236 g_list_delete_link (priv->resources,
240 if (priv->message_queue) {
241 /* send messages without usual delay */
242 while (!g_queue_is_empty (priv->message_queue)) {
244 process_queue (resource_group);
246 g_free (g_queue_pop_head
247 (priv->message_queue));
250 g_queue_free (priv->message_queue);
251 priv->message_queue = NULL;
254 if (priv->message_src) {
255 g_source_destroy (priv->message_src);
256 priv->message_src = NULL;
259 if (priv->timeout_src) {
260 g_source_destroy (priv->timeout_src);
261 priv->timeout_src = NULL;
265 if (g_signal_handler_is_connected
267 priv->message_received_id)) {
268 g_signal_handler_disconnect
270 priv->message_received_id);
273 g_object_unref (priv->client);
277 G_OBJECT_CLASS (gssdp_resource_group_parent_class)->dispose (object);
281 gssdp_resource_group_class_init (GSSDPResourceGroupClass *klass)
283 GObjectClass *object_class;
285 object_class = G_OBJECT_CLASS (klass);
287 object_class->set_property = gssdp_resource_group_set_property;
288 object_class->get_property = gssdp_resource_group_get_property;
289 object_class->dispose = gssdp_resource_group_dispose;
291 g_type_class_add_private (klass, sizeof (GSSDPResourceGroupPrivate));
294 * GSSDPResourceGroup:client:
296 * The #GSSDPClient to use.
298 g_object_class_install_property
304 "The associated client.",
306 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
307 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
308 G_PARAM_STATIC_BLURB));
311 * GSSDPResourceGroup:max-age:
313 * The number of seconds our advertisements are valid.
315 g_object_class_install_property
321 "The number of seconds advertisements are valid.",
324 SSDP_DEFAULT_MAX_AGE,
326 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
327 G_PARAM_STATIC_BLURB));
330 * GSSDPResourceGroup:available:
332 * Whether this group of resources is available or not.
334 g_object_class_install_property
340 "Whether this group of resources is available or "
344 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
345 G_PARAM_STATIC_BLURB));
348 * GSSDPResourceGroup:message-delay:
350 * The minimum number of milliseconds between SSDP messages.
351 * The default is 120 based on DLNA specification.
353 g_object_class_install_property
359 "The minimum number of milliseconds between SSDP "
363 DEFAULT_MESSAGE_DELAY,
365 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
366 G_PARAM_STATIC_BLURB));
370 * gssdp_resource_group_new:
371 * @client: The #GSSDPClient to associate with
373 * Return value: A new #GSSDPResourceGroup object.
376 gssdp_resource_group_new (GSSDPClient *client)
378 return g_object_new (GSSDP_TYPE_RESOURCE_GROUP,
384 * Sets the #GSSDPClient @resource_group is associated with @client
387 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
390 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
391 g_return_if_fail (GSSDP_IS_CLIENT (client));
393 resource_group->priv->client = g_object_ref (client);
395 resource_group->priv->message_received_id =
396 g_signal_connect_object (resource_group->priv->client,
398 G_CALLBACK (message_received_cb),
402 g_object_notify (G_OBJECT (resource_group), "client");
406 * gssdp_resource_group_get_client:
407 * @resource_group: A #GSSDPResourceGroup
409 * Returns: (transfer none): The #GSSDPClient @resource_group is associated with.
412 gssdp_resource_group_get_client (GSSDPResourceGroup *resource_group)
414 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), NULL);
416 return resource_group->priv->client;
420 * gssdp_resource_group_set_max_age:
421 * @resource_group: A #GSSDPResourceGroup
422 * @max_age: The number of seconds advertisements are valid
424 * Sets the number of seconds advertisements are valid to @max_age.
427 gssdp_resource_group_set_max_age (GSSDPResourceGroup *resource_group,
430 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
432 if (resource_group->priv->max_age == max_age)
435 resource_group->priv->max_age = max_age;
437 g_object_notify (G_OBJECT (resource_group), "max-age");
441 * gssdp_resource_group_get_max_age:
442 * @resource_group: A #GSSDPResourceGroup
444 * Return value: The number of seconds advertisements are valid.
447 gssdp_resource_group_get_max_age (GSSDPResourceGroup *resource_group)
449 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
451 return resource_group->priv->max_age;
455 * gssdp_resource_group_set_message_delay:
456 * @resource_group: A #GSSDPResourceGroup
457 * @message_delay: The message delay in ms.
459 * Sets the minimum time between each SSDP message.
462 gssdp_resource_group_set_message_delay (GSSDPResourceGroup *resource_group,
465 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
467 if (resource_group->priv->message_delay == message_delay)
470 resource_group->priv->message_delay = message_delay;
472 g_object_notify (G_OBJECT (resource_group), "message-delay");
476 * gssdp_resource_group_get_message_delay:
477 * @resource_group: A #GSSDPResourceGroup
479 * Return value: the minimum time between each SSDP message in ms.
482 gssdp_resource_group_get_message_delay (GSSDPResourceGroup *resource_group)
484 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
486 return resource_group->priv->message_delay;
490 send_initial_resource_byebye (Resource *resource)
492 if (!resource->initial_byebye_sent) {
493 /* Unannounce before first announce. This is
494 done to minimize the possibility of
495 control points thinking that this is just
497 resource_byebye (resource);
499 resource->initial_byebye_sent = TRUE;
504 send_announcement_set (GList *resources, GFunc message_function)
508 for (i = 0; i < DEFAULT_ANNOUNCEMENT_SET_SIZE; i++) {
509 g_list_foreach (resources, message_function, NULL);
514 * gssdp_resource_group_set_available:
515 * @resource_group: A #GSSDPResourceGroup
516 * @available: TRUE if @resource_group should be available (advertised)
518 * Sets @resource_group<!-- -->s availability to @available. Changing
519 * @resource_group<!-- -->s availability causes it to announce its new state
520 * to listening SSDP clients.
523 gssdp_resource_group_set_available (GSSDPResourceGroup *resource_group,
526 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
528 if (resource_group->priv->available == available)
531 resource_group->priv->available = available;
536 /* We want to re-announce at least 3 times before the resource
537 * group expires to cope with the unrelialble nature of UDP.
539 * Read the paragraphs about 'CACHE-CONTROL' on pages 21-22 of
540 * UPnP Device Architecture Document v1.1 for further details.
542 timeout = resource_group->priv->max_age;
543 if (G_LIKELY (timeout > 6))
544 timeout = (timeout / 3) - 1;
546 /* Add re-announcement timer */
547 resource_group->priv->timeout_src =
548 g_timeout_source_new_seconds (timeout);
549 g_source_set_callback (resource_group->priv->timeout_src,
550 resource_group_timeout,
551 resource_group, NULL);
553 g_source_attach (resource_group->priv->timeout_src,
554 g_main_context_get_thread_default ());
556 g_source_unref (resource_group->priv->timeout_src);
558 /* Make sure initial byebyes are sent grouped before initial
560 send_announcement_set (resource_group->priv->resources,
561 (GFunc) send_initial_resource_byebye);
563 send_announcement_set (resource_group->priv->resources,
564 (GFunc) resource_alive);
566 /* Unannounce all resources */
567 send_announcement_set (resource_group->priv->resources,
568 (GFunc) resource_byebye);
570 /* Remove re-announcement timer */
571 g_source_destroy (resource_group->priv->timeout_src);
572 resource_group->priv->timeout_src = NULL;
575 g_object_notify (G_OBJECT (resource_group), "available");
579 * gssdp_resource_group_get_available:
580 * @resource_group: A #GSSDPResourceGroup
582 * Return value: TRUE if @resource_group is available (advertised).
585 gssdp_resource_group_get_available (GSSDPResourceGroup *resource_group)
587 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), FALSE);
589 return resource_group->priv->available;
593 * gssdp_resource_group_add_resource:
594 * @resource_group: An @GSSDPResourceGroup
595 * @target: The resource's target
596 * @usn: The resource's USN
597 * @locations: (element-type utf8): A #GList of the resource's locations
599 * Adds a resource with target @target, USN @usn, and locations @locations
600 * to @resource_group.
602 * Return value: The ID of the added resource.
605 gssdp_resource_group_add_resource (GSSDPResourceGroup *resource_group,
614 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
615 g_return_val_if_fail (target != NULL, 0);
616 g_return_val_if_fail (usn != NULL, 0);
617 g_return_val_if_fail (locations != NULL, 0);
619 resource = g_slice_new0 (Resource);
621 resource->resource_group = resource_group;
623 resource->target = g_strdup (target);
624 resource->usn = g_strdup (usn);
627 resource->target_regex = create_target_regex (target, &resource->version, &error);
629 g_warning ("Error compiling regular expression for '%s': %s",
633 g_error_free (error);
634 resource_free (resource);
639 resource->initial_byebye_sent = FALSE;
641 for (l = locations; l; l = l->next) {
642 resource->locations = g_list_append (resource->locations,
646 resource_group->priv->resources =
647 g_list_prepend (resource_group->priv->resources, resource);
649 resource->id = ++resource_group->priv->last_resource_id;
651 if (resource_group->priv->available)
652 resource_alive (resource);
658 * gssdp_resource_group_add_resource_simple:
659 * @resource_group: An @GSSDPResourceGroup
660 * @target: The resource's target
661 * @usn: The resource's USN
662 * @location: The resource's location
664 * Adds a resource with target @target, USN @usn, and location @location
665 * to @resource_group.
667 * Return value: The ID of the added resource.
670 gssdp_resource_group_add_resource_simple (GSSDPResourceGroup *resource_group,
673 const char *location)
675 GList *locations = NULL;
678 locations = g_list_append (locations, (gpointer) location);
679 resource_id = gssdp_resource_group_add_resource (resource_group, target, usn, locations);
681 g_list_free (locations);
687 * gssdp_resource_group_remove_resource:
688 * @resource_group: An @GSSDPResourceGroup
689 * @resource_id: The ID of the resource to remove
691 * Removes the resource with ID @resource_id from @resource_group.
694 gssdp_resource_group_remove_resource (GSSDPResourceGroup *resource_group,
699 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
700 g_return_if_fail (resource_id > 0);
702 for (l = resource_group->priv->resources; l; l = l->next) {
707 if (resource->id == resource_id) {
708 resource_group->priv->resources =
709 g_list_remove (resource_group->priv->resources,
712 resource_free (resource);
720 * Called to re-announce all resources periodically
723 resource_group_timeout (gpointer user_data)
725 GSSDPResourceGroup *resource_group;
727 resource_group = GSSDP_RESOURCE_GROUP (user_data);
729 send_announcement_set (resource_group->priv->resources,
730 (GFunc) resource_alive);
739 message_received_cb (GSSDPClient *client,
742 _GSSDPMessageType type,
743 SoupMessageHeaders *headers,
746 GSSDPResourceGroup *resource_group;
747 const char *target, *mx_str, *version_str, *man;
752 resource_group = GSSDP_RESOURCE_GROUP (user_data);
754 /* Only process if we are available */
755 if (!resource_group->priv->available)
758 /* We only handle discovery requests */
759 if (type != _GSSDP_DISCOVERY_REQUEST)
763 target = soup_message_headers_get_one (headers, "ST");
765 g_warning ("Discovery request did not have an ST header");
770 /* Is this the "ssdp:all" target? */
771 want_all = (strcmp (target, GSSDP_ALL_RESOURCES) == 0);
774 mx_str = soup_message_headers_get_one (headers, "MX");
775 if (!mx_str || atoi (mx_str) <= 0) {
776 g_warning ("Discovery request did not have a valid MX header");
781 man = soup_message_headers_get_one (headers, "MAN");
783 g_warning ("Discovery request did not have a valid MAN header");
790 /* Extract version */
791 version_str = get_version_for_target ((char *) target);
792 if (version_str != NULL)
793 version = atoi (version_str);
797 /* Find matching resource */
798 for (l = resource_group->priv->resources; l; l = l->next) {
804 (g_regex_match (resource->target_regex,
808 version <= resource->version)) {
811 DiscoveryResponse *response;
813 /* Get a random timeout from the interval [0, mx] */
814 timeout = g_random_int_range (0, mx * 1000);
816 /* Prepare response */
817 response = g_slice_new (DiscoveryResponse);
819 response->dest_ip = g_strdup (from_ip);
820 response->dest_port = from_port;
821 response->resource = resource;
824 response->target = g_strdup (resource->target);
826 response->target = g_strdup (target);
829 response->timeout_src = g_timeout_source_new (timeout);
830 g_source_set_callback (response->timeout_src,
831 discovery_response_timeout,
834 g_source_attach (response->timeout_src,
835 g_main_context_get_thread_default ());
837 g_source_unref (response->timeout_src);
839 /* Add to resource */
840 resource->responses =
841 g_list_prepend (resource->responses, response);
847 * Construct the AL (Alternative Locations) header for @resource
850 construct_al (Resource *resource)
852 if (resource->locations->next) {
856 al_string = g_string_new ("AL: ");
858 for (l = resource->locations->next; l; l = l->next) {
859 g_string_append_c (al_string, '<');
860 g_string_append (al_string, l->data);
861 g_string_append_c (al_string, '>');
864 g_string_append (al_string, "\r\n");
866 return g_string_free (al_string, FALSE);
872 construct_usn (const char *usn,
873 const char *response_target,
874 const char *resource_target)
880 needle = strstr (usn, resource_target);
882 return g_strdup (usn);
884 prefix = g_strndup (usn, needle - usn);
885 st = g_strconcat (prefix, response_target, NULL);
893 * Send a discovery response
896 discovery_response_timeout (gpointer user_data)
898 DiscoveryResponse *response;
901 char *al, *date_str, *message;
905 response = user_data;
908 client = response->resource->resource_group->priv->client;
910 max_age = response->resource->resource_group->priv->max_age;
912 al = construct_al (response->resource);
913 usn = construct_usn (response->resource->usn,
915 response->resource->target);
916 date = soup_date_new_from_now (0);
917 date_str = soup_date_to_string (date, SOUP_DATE_HTTP);
918 soup_date_free (date);
920 message = g_strdup_printf (SSDP_DISCOVERY_RESPONSE,
921 (char *) response->resource->locations->data,
924 gssdp_client_get_server_id (client),
929 _gssdp_client_send_message (client,
933 _GSSDP_DISCOVERY_RESPONSE);
940 discovery_response_free (response);
946 * Free a DiscoveryResponse structure and its contained data
949 discovery_response_free (DiscoveryResponse *response)
951 response->resource->responses =
952 g_list_remove (response->resource->responses, response);
954 g_source_destroy (response->timeout_src);
956 g_free (response->dest_ip);
957 g_free (response->target);
959 g_slice_free (DiscoveryResponse, response);
963 * Send the next queued message, if any
966 process_queue (gpointer data)
968 GSSDPResourceGroup *resource_group;
970 resource_group = GSSDP_RESOURCE_GROUP (data);
972 if (g_queue_is_empty (resource_group->priv->message_queue)) {
973 /* this is the timeout after last message in queue */
974 resource_group->priv->message_src = NULL;
981 client = resource_group->priv->client;
982 message = g_queue_pop_head
983 (resource_group->priv->message_queue);
985 _gssdp_client_send_message (client,
989 _GSSDP_DISCOVERY_RESPONSE);
997 * Add a message to sending queue
999 * Do not free @message.
1002 queue_message (GSSDPResourceGroup *resource_group,
1005 g_queue_push_tail (resource_group->priv->message_queue,
1008 if (resource_group->priv->message_src == NULL) {
1009 /* nothing in the queue: process message immediately
1010 and add a timeout for (possible) next message */
1011 process_queue (resource_group);
1012 resource_group->priv->message_src = g_timeout_source_new (
1013 resource_group->priv->message_delay);
1014 g_source_set_callback (resource_group->priv->message_src,
1015 process_queue, resource_group, NULL);
1016 g_source_attach (resource_group->priv->message_src,
1017 g_main_context_get_thread_default ());
1018 g_source_unref (resource_group->priv->message_src);
1023 * Send ssdp:alive message for @resource
1026 resource_alive (Resource *resource)
1028 GSSDPClient *client;
1032 /* Send initial byebye if not sent already */
1033 send_initial_resource_byebye (resource);
1036 client = resource->resource_group->priv->client;
1038 max_age = resource->resource_group->priv->max_age;
1040 al = construct_al (resource);
1042 message = g_strdup_printf (SSDP_ALIVE_MESSAGE,
1044 (char *) resource->locations->data,
1046 gssdp_client_get_server_id (client),
1050 queue_message (resource->resource_group, message);
1056 * Send ssdp:byebye message for @resource
1059 resource_byebye (Resource *resource)
1064 message = g_strdup_printf (SSDP_BYEBYE_MESSAGE,
1068 queue_message (resource->resource_group, message);
1072 * Free a Resource structure and its contained data
1075 resource_free (Resource *resource)
1077 while (resource->responses)
1078 discovery_response_free (resource->responses->data);
1080 if (resource->resource_group->priv->available)
1081 resource_byebye (resource);
1083 g_free (resource->usn);
1084 g_free (resource->target);
1086 if (resource->target_regex)
1087 g_regex_unref (resource->target_regex);
1089 while (resource->locations) {
1090 g_free (resource->locations->data);
1091 resource->locations = g_list_delete_link (resource->locations,
1092 resource->locations);
1095 g_slice_free (Resource, resource);
1098 /* Gets you the pointer to the version part in the target string */
1100 get_version_for_target (char *target)
1104 if (strncmp (target, "urn:", 4) != 0) {
1105 /* target is not a URN so no version. */
1109 version = g_strrstr (target, ":") + 1;
1110 if (version == NULL ||
1111 !g_regex_match_simple (VERSION_PATTERN, version, 0, 0))
1118 create_target_regex (const char *target, guint *version, GError **error)
1125 /* Make sure we have enough room for version pattern */
1126 pattern = g_strndup (target,
1127 strlen (target) + strlen (VERSION_PATTERN));
1129 version_str = get_version_for_target (pattern);
1130 if (version_str != NULL) {
1131 *version = atoi (version_str);
1132 strcpy (version_str, VERSION_PATTERN);
1135 regex = g_regex_new (pattern, 0, 0, error);