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., 59 Temple Place - Suite 330,
19 * Boston, MA 02111-1307, 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;
87 gboolean initial_alive_sent;
99 #define DEFAULT_MESSAGE_DELAY 20
100 #define VERSION_PATTERN "[0-9]+$"
102 /* Function prototypes */
104 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
105 GSSDPClient *client);
107 resource_group_timeout (gpointer user_data);
109 message_received_cb (GSSDPClient *client,
112 _GSSDPMessageType type,
113 SoupMessageHeaders *headers,
116 resource_alive (Resource *resource);
118 resource_byebye (Resource *resource);
120 resource_free (Resource *resource);
122 discovery_response_timeout (gpointer user_data);
124 discovery_response_free (DiscoveryResponse *response);
126 process_queue (gpointer data);
128 get_version_for_target (char *target);
130 create_target_regex (const char *target,
134 gssdp_resource_group_init (GSSDPResourceGroup *resource_group)
136 resource_group->priv = G_TYPE_INSTANCE_GET_PRIVATE
138 GSSDP_TYPE_RESOURCE_GROUP,
139 GSSDPResourceGroupPrivate);
141 resource_group->priv->max_age = SSDP_DEFAULT_MAX_AGE;
142 resource_group->priv->message_delay = DEFAULT_MESSAGE_DELAY;
144 resource_group->priv->message_queue = g_queue_new ();
148 gssdp_resource_group_get_property (GObject *object,
153 GSSDPResourceGroup *resource_group;
155 resource_group = GSSDP_RESOURCE_GROUP (object);
157 switch (property_id) {
161 gssdp_resource_group_get_client (resource_group));
166 gssdp_resource_group_get_max_age (resource_group));
171 gssdp_resource_group_get_available (resource_group));
173 case PROP_MESSAGE_DELAY:
176 gssdp_resource_group_get_message_delay
180 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
186 gssdp_resource_group_set_property (GObject *object,
191 GSSDPResourceGroup *resource_group;
193 resource_group = GSSDP_RESOURCE_GROUP (object);
195 switch (property_id) {
197 gssdp_resource_group_set_client (resource_group,
198 g_value_get_object (value));
201 gssdp_resource_group_set_max_age (resource_group,
202 g_value_get_long (value));
205 gssdp_resource_group_set_available
206 (resource_group, g_value_get_boolean (value));
208 case PROP_MESSAGE_DELAY:
209 gssdp_resource_group_set_message_delay
210 (resource_group, g_value_get_uint (value));
213 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
219 gssdp_resource_group_dispose (GObject *object)
221 GSSDPResourceGroup *resource_group;
222 GSSDPResourceGroupPrivate *priv;
224 resource_group = GSSDP_RESOURCE_GROUP (object);
225 priv = resource_group->priv;
227 while (priv->resources) {
228 resource_free (priv->resources->data);
230 g_list_delete_link (priv->resources,
234 if (priv->message_queue) {
235 /* send messages without usual delay */
236 while (!g_queue_is_empty (priv->message_queue)) {
238 process_queue (resource_group);
240 g_free (g_queue_pop_head
241 (priv->message_queue));
244 g_queue_free (priv->message_queue);
245 priv->message_queue = NULL;
248 if (priv->message_src) {
249 g_source_destroy (priv->message_src);
250 priv->message_src = NULL;
253 if (priv->timeout_src) {
254 g_source_destroy (priv->timeout_src);
255 priv->timeout_src = NULL;
259 if (g_signal_handler_is_connected
261 priv->message_received_id)) {
262 g_signal_handler_disconnect
264 priv->message_received_id);
267 g_object_unref (priv->client);
271 G_OBJECT_CLASS (gssdp_resource_group_parent_class)->dispose (object);
275 gssdp_resource_group_class_init (GSSDPResourceGroupClass *klass)
277 GObjectClass *object_class;
279 object_class = G_OBJECT_CLASS (klass);
281 object_class->set_property = gssdp_resource_group_set_property;
282 object_class->get_property = gssdp_resource_group_get_property;
283 object_class->dispose = gssdp_resource_group_dispose;
285 g_type_class_add_private (klass, sizeof (GSSDPResourceGroupPrivate));
288 * GSSDPResourceGroup:client
290 * The #GSSDPClient to use.
292 g_object_class_install_property
298 "The associated client.",
300 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
301 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
302 G_PARAM_STATIC_BLURB));
305 * GSSDPResourceGroup:max-age
307 * The number of seconds our advertisements are valid.
309 g_object_class_install_property
315 "The number of seconds advertisements are valid.",
318 SSDP_DEFAULT_MAX_AGE,
320 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
321 G_PARAM_STATIC_BLURB));
324 * GSSDPResourceGroup:available
326 * Whether this group of resources is available or not.
328 g_object_class_install_property
334 "Whether this group of resources is available or "
338 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
339 G_PARAM_STATIC_BLURB));
342 * GSSDPResourceGroup:message-delay
344 * The minimum number of milliseconds between SSDP messages.
345 * The default is 20 based on DLNA specification.
347 g_object_class_install_property
353 "The minimum number of milliseconds between SSDP "
357 DEFAULT_MESSAGE_DELAY,
359 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
360 G_PARAM_STATIC_BLURB));
364 * gssdp_resource_group_new
365 * @client: The #GSSDPClient to associate with
367 * Return value: A new #GSSDPResourceGroup object.
370 gssdp_resource_group_new (GSSDPClient *client)
372 return g_object_new (GSSDP_TYPE_RESOURCE_GROUP,
378 * Sets the #GSSDPClient @resource_group is associated with @client
381 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
384 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
385 g_return_if_fail (GSSDP_IS_CLIENT (client));
387 resource_group->priv->client = g_object_ref (client);
389 resource_group->priv->message_received_id =
390 g_signal_connect_object (resource_group->priv->client,
392 G_CALLBACK (message_received_cb),
396 g_object_notify (G_OBJECT (resource_group), "client");
400 * gssdp_resource_group_get_client
401 * @resource_group: A #GSSDPResourceGroup
403 * Return value: The #GSSDPClient @resource_group is associated with.
406 gssdp_resource_group_get_client (GSSDPResourceGroup *resource_group)
408 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), NULL);
410 return resource_group->priv->client;
414 * gssdp_resource_group_set_max_age
415 * @resource_group: A #GSSDPResourceGroup
416 * @max_age: The number of seconds advertisements are valid
418 * Sets the number of seconds advertisements are valid to @max_age.
421 gssdp_resource_group_set_max_age (GSSDPResourceGroup *resource_group,
424 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
426 if (resource_group->priv->max_age == max_age)
429 resource_group->priv->max_age = max_age;
431 g_object_notify (G_OBJECT (resource_group), "max-age");
435 * gssdp_resource_group_get_max_age
436 * @resource_group: A #GSSDPResourceGroup
438 * Return value: The number of seconds advertisements are valid.
441 gssdp_resource_group_get_max_age (GSSDPResourceGroup *resource_group)
443 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
445 return resource_group->priv->max_age;
449 * gssdp_resource_group_set_message_delay
450 * @resource_group: A #GSSDPResourceGroup
451 * @message_delay: The message delay in ms.
453 * Sets the minimum time between each SSDP message.
456 gssdp_resource_group_set_message_delay (GSSDPResourceGroup *resource_group,
459 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
461 if (resource_group->priv->message_delay == message_delay)
464 resource_group->priv->message_delay = message_delay;
466 g_object_notify (G_OBJECT (resource_group), "message-delay");
470 * gssdp_resource_group_get_message_delay
471 * @resource_group: A #GSSDPResourceGroup
473 * Return value: the minimum time between each SSDP message in ms.
476 gssdp_resource_group_get_message_delay (GSSDPResourceGroup *resource_group)
478 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
480 return resource_group->priv->message_delay;
484 * gssdp_resource_group_set_available
485 * @resource_group: A #GSSDPResourceGroup
486 * @available: TRUE if @resource_group should be available (advertised)
488 * Sets @resource_group<!-- -->s availability to @available. Changing
489 * @resource_group<!-- -->s availability causes it to announce its new state
490 * to listening SSDP clients.
493 gssdp_resource_group_set_available (GSSDPResourceGroup *resource_group,
498 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
500 if (resource_group->priv->available == available)
503 resource_group->priv->available = available;
506 GMainContext *context;
509 /* We want to re-announce at least 3 times before the resource
510 * group expires to cope with the unrelialble nature of UDP.
512 * Read the paragraphs about 'CACHE-CONTROL' on pages 21-22 of
513 * UPnP Device Architecture Document v1.1 for further details.
515 timeout = resource_group->priv->max_age;
516 if (G_LIKELY (timeout > 6))
517 timeout = (timeout / 3) - 1;
519 /* Add re-announcement timer */
520 resource_group->priv->timeout_src =
521 g_timeout_source_new_seconds (timeout);
522 g_source_set_callback (resource_group->priv->timeout_src,
523 resource_group_timeout,
524 resource_group, NULL);
526 context = gssdp_client_get_main_context
527 (resource_group->priv->client);
528 g_source_attach (resource_group->priv->timeout_src, context);
530 g_source_unref (resource_group->priv->timeout_src);
532 /* Announce all resources */
533 for (l = resource_group->priv->resources; l; l = l->next)
534 resource_alive (l->data);
536 /* Unannounce all resources */
537 for (l = resource_group->priv->resources; l; l = l->next)
538 resource_byebye (l->data);
540 /* Remove re-announcement timer */
541 g_source_destroy (resource_group->priv->timeout_src);
542 resource_group->priv->timeout_src = NULL;
545 g_object_notify (G_OBJECT (resource_group), "available");
549 * gssdp_resource_group_get_available
550 * @resource_group: A #GSSDPResourceGroup
552 * Return value: TRUE if @resource_group is available (advertised).
555 gssdp_resource_group_get_available (GSSDPResourceGroup *resource_group)
557 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), FALSE);
559 return resource_group->priv->available;
563 * gssdp_resource_group_add_resource
564 * @resource_group: An @GSSDPResourceGroup
565 * @target: The resource's target
566 * @usn: The resource's USN
567 * @locations: A #GList of the resource's locations
569 * Adds a resource with target @target, USN @usn, and locations @locations
570 * to @resource_group.
572 * Return value: The ID of the added resource.
575 gssdp_resource_group_add_resource (GSSDPResourceGroup *resource_group,
584 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
585 g_return_val_if_fail (target != NULL, 0);
586 g_return_val_if_fail (usn != NULL, 0);
587 g_return_val_if_fail (locations != NULL, 0);
589 resource = g_slice_new0 (Resource);
591 resource->resource_group = resource_group;
593 resource->target = g_strdup (target);
594 resource->usn = g_strdup (usn);
597 resource->target_regex = create_target_regex (target, &error);
599 g_warning ("Error compiling regular expression for '%s': %s",
603 g_error_free (error);
604 resource_free (resource);
609 resource->initial_alive_sent = FALSE;
611 for (l = locations; l; l = l->next) {
612 resource->locations = g_list_append (resource->locations,
616 resource_group->priv->resources =
617 g_list_prepend (resource_group->priv->resources, resource);
619 resource->id = ++resource_group->priv->last_resource_id;
621 if (resource_group->priv->available)
622 resource_alive (resource);
628 * gssdp_resource_group_add_resource_simple
629 * @resource_group: An @GSSDPResourceGroup
630 * @target: The resource's target
631 * @usn: The resource's USN
632 * @location: The resource's location
634 * Adds a resource with target @target, USN @usn, and location @location
635 * to @resource_group.
637 * Return value: The ID of the added resource.
640 gssdp_resource_group_add_resource_simple (GSSDPResourceGroup *resource_group,
643 const char *location)
648 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
649 g_return_val_if_fail (target != NULL, 0);
650 g_return_val_if_fail (usn != NULL, 0);
651 g_return_val_if_fail (location != NULL, 0);
653 resource = g_slice_new0 (Resource);
655 resource->resource_group = resource_group;
657 resource->target = g_strdup (target);
658 resource->usn = g_strdup (usn);
661 resource->target_regex = create_target_regex (target, &error);
663 g_warning ("Error compiling regular expression for '%s': %s",
667 g_error_free (error);
668 resource_free (resource);
673 resource->locations = g_list_append (resource->locations,
674 g_strdup (location));
676 resource_group->priv->resources =
677 g_list_prepend (resource_group->priv->resources, resource);
679 resource->id = ++resource_group->priv->last_resource_id;
681 if (resource_group->priv->available)
682 resource_alive (resource);
688 * gssdp_resource_group_remove_resource
689 * @resource_group: An @GSSDPResourceGroup
690 * @resource_id: The ID of the resource to remove
692 * Removes the resource with ID @resource_id from @resource_group.
695 gssdp_resource_group_remove_resource (GSSDPResourceGroup *resource_group,
700 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
701 g_return_if_fail (resource_id > 0);
703 for (l = resource_group->priv->resources; l; l = l->next) {
708 if (resource->id == resource_id) {
709 resource_group->priv->resources =
710 g_list_remove (resource_group->priv->resources,
713 resource_free (resource);
721 * Called to re-announce all resources periodically
724 resource_group_timeout (gpointer user_data)
726 GSSDPResourceGroup *resource_group;
729 resource_group = GSSDP_RESOURCE_GROUP (user_data);
731 /* Re-announce all resources */
732 for (l = resource_group->priv->resources; l; l = l->next)
733 resource_alive (l->data);
742 message_received_cb (GSSDPClient *client,
745 _GSSDPMessageType type,
746 SoupMessageHeaders *headers,
749 GSSDPResourceGroup *resource_group;
750 const char *target, *mx_str;
755 resource_group = GSSDP_RESOURCE_GROUP (user_data);
757 /* Only process if we are available */
758 if (!resource_group->priv->available)
761 /* We only handle discovery requests */
762 if (type != _GSSDP_DISCOVERY_REQUEST)
766 target = soup_message_headers_get_one (headers, "ST");
768 g_warning ("Discovery request did not have an ST header");
773 /* Is this the "ssdp:all" target? */
774 want_all = (strcmp (target, GSSDP_ALL_RESOURCES) == 0);
777 mx_str = soup_message_headers_get_one (headers, "MX");
781 mx = SSDP_DEFAULT_MX;
783 /* Find matching resource */
784 for (l = resource_group->priv->resources; l; l = l->next) {
790 g_regex_match (resource->target_regex,
796 DiscoveryResponse *response;
797 GMainContext *context;
799 /* Get a random timeout from the interval [0, mx] */
800 timeout = g_random_int_range (0, mx * 1000);
802 /* Prepare response */
803 response = g_slice_new (DiscoveryResponse);
805 response->dest_ip = g_strdup (from_ip);
806 response->dest_port = from_port;
807 response->resource = resource;
810 response->target = g_strdup (resource->target);
812 response->target = g_strdup (target);
815 response->timeout_src = g_timeout_source_new (timeout);
816 g_source_set_callback (response->timeout_src,
817 discovery_response_timeout,
820 context = gssdp_client_get_main_context (client);
821 g_source_attach (response->timeout_src, context);
823 g_source_unref (response->timeout_src);
825 /* Add to resource */
826 resource->responses =
827 g_list_prepend (resource->responses, response);
833 * Construct the AL (Alternative Locations) header for @resource
836 construct_al (Resource *resource)
838 if (resource->locations->next) {
842 al_string = g_string_new ("AL: ");
844 for (l = resource->locations->next; l; l = l->next) {
845 g_string_append_c (al_string, '<');
846 g_string_append (al_string, l->data);
847 g_string_append_c (al_string, '>');
850 g_string_append (al_string, "\r\n");
852 return g_string_free (al_string, FALSE);
858 * Send a discovery response
861 discovery_response_timeout (gpointer user_data)
863 DiscoveryResponse *response;
866 char *al, *date_str, *message;
869 response = user_data;
872 client = response->resource->resource_group->priv->client;
874 max_age = response->resource->resource_group->priv->max_age;
876 al = construct_al (response->resource);
878 date = soup_date_new_from_now (0);
879 date_str = soup_date_to_string (date, SOUP_DATE_HTTP);
880 soup_date_free (date);
882 message = g_strdup_printf (SSDP_DISCOVERY_RESPONSE,
883 (char *) response->resource->locations->data,
885 response->resource->usn,
886 gssdp_client_get_server_id (client),
891 _gssdp_client_send_message (client,
900 discovery_response_free (response);
906 * Free a DiscoveryResponse structure and its contained data
909 discovery_response_free (DiscoveryResponse *response)
911 response->resource->responses =
912 g_list_remove (response->resource->responses, response);
914 g_source_destroy (response->timeout_src);
916 g_free (response->dest_ip);
917 g_free (response->target);
919 g_slice_free (DiscoveryResponse, response);
923 * Send the next queued message, if any
926 process_queue (gpointer data)
928 GSSDPResourceGroup *resource_group;
930 resource_group = GSSDP_RESOURCE_GROUP (data);
932 if (g_queue_is_empty (resource_group->priv->message_queue)) {
933 /* this is the timeout after last message in queue */
934 resource_group->priv->message_src = NULL;
941 client = resource_group->priv->client;
942 message = g_queue_pop_head
943 (resource_group->priv->message_queue);
945 _gssdp_client_send_message (client,
956 * Add a message to sending queue
958 * Do not free @message.
961 queue_message (GSSDPResourceGroup *resource_group,
964 g_queue_push_tail (resource_group->priv->message_queue,
967 if (resource_group->priv->message_src == NULL) {
968 /* nothing in the queue: process message immediately
969 and add a timeout for (possible) next message */
970 GMainContext *context;
972 process_queue (resource_group);
973 resource_group->priv->message_src = g_timeout_source_new (
974 resource_group->priv->message_delay);
975 g_source_set_callback (resource_group->priv->message_src,
976 process_queue, resource_group, NULL);
977 context = gssdp_client_get_main_context (
978 resource_group->priv->client);
979 g_source_attach (resource_group->priv->message_src, context);
980 g_source_unref (resource_group->priv->message_src);
985 * Send ssdp:alive message for @resource
988 resource_alive (Resource *resource)
994 if (!resource->initial_alive_sent) {
995 /* Unannounce before first announce. This is done to
996 minimize the possibility of control points thinking
997 that this is just a reannouncement. */
998 resource_byebye (resource);
1000 resource->initial_alive_sent = TRUE;
1004 client = resource->resource_group->priv->client;
1006 max_age = resource->resource_group->priv->max_age;
1008 al = construct_al (resource);
1010 message = g_strdup_printf (SSDP_ALIVE_MESSAGE,
1012 (char *) resource->locations->data,
1014 gssdp_client_get_server_id (client),
1018 queue_message (resource->resource_group, message);
1024 * Send ssdp:byebye message for @resource
1027 resource_byebye (Resource *resource)
1032 message = g_strdup_printf (SSDP_BYEBYE_MESSAGE,
1036 queue_message (resource->resource_group, message);
1040 * Free a Resource structure and its contained data
1043 resource_free (Resource *resource)
1045 while (resource->responses)
1046 discovery_response_free (resource->responses->data);
1048 if (resource->resource_group->priv->available)
1049 resource_byebye (resource);
1051 g_free (resource->usn);
1052 g_free (resource->target);
1054 if (resource->target_regex)
1055 g_regex_unref (resource->target_regex);
1057 while (resource->locations) {
1058 g_free (resource->locations->data);
1059 resource->locations = g_list_delete_link (resource->locations,
1060 resource->locations);
1063 g_slice_free (Resource, resource);
1066 /* Gets you the pointer to the version part in the target string */
1068 get_version_for_target (char *target)
1072 if (strncmp (target, "urn:", 4) != 0) {
1073 /* target is not a URN so no version. */
1077 version = g_strrstr (target, ":") + 1;
1078 if (version == NULL ||
1079 !g_regex_match_simple (VERSION_PATTERN, version, 0, 0))
1086 create_target_regex (const char *target, GError **error)
1092 /* Make sure we have enough room for version pattern */
1093 pattern = g_strndup (target,
1094 strlen (target) + strlen (VERSION_PATTERN));
1096 version = get_version_for_target (pattern);
1097 if (version != NULL)
1098 strcpy (version, VERSION_PATTERN);
1100 regex = g_regex_new (pattern, 0, 0, error);