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
101 /* Function prototypes */
103 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
104 GSSDPClient *client);
106 resource_group_timeout (gpointer user_data);
108 message_received_cb (GSSDPClient *client,
111 _GSSDPMessageType type,
112 SoupMessageHeaders *headers,
115 resource_alive (Resource *resource);
117 resource_byebye (Resource *resource);
119 resource_free (Resource *resource);
121 discovery_response_timeout (gpointer user_data);
123 discovery_response_free (DiscoveryResponse *response);
125 process_queue (gpointer data);
127 create_target_regex (const char *target,
131 gssdp_resource_group_init (GSSDPResourceGroup *resource_group)
133 resource_group->priv = G_TYPE_INSTANCE_GET_PRIVATE
135 GSSDP_TYPE_RESOURCE_GROUP,
136 GSSDPResourceGroupPrivate);
138 resource_group->priv->max_age = SSDP_DEFAULT_MAX_AGE;
139 resource_group->priv->message_delay = DEFAULT_MESSAGE_DELAY;
141 resource_group->priv->message_queue = g_queue_new ();
145 gssdp_resource_group_get_property (GObject *object,
150 GSSDPResourceGroup *resource_group;
152 resource_group = GSSDP_RESOURCE_GROUP (object);
154 switch (property_id) {
158 gssdp_resource_group_get_client (resource_group));
163 gssdp_resource_group_get_max_age (resource_group));
168 gssdp_resource_group_get_available (resource_group));
170 case PROP_MESSAGE_DELAY:
173 gssdp_resource_group_get_message_delay
177 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
183 gssdp_resource_group_set_property (GObject *object,
188 GSSDPResourceGroup *resource_group;
190 resource_group = GSSDP_RESOURCE_GROUP (object);
192 switch (property_id) {
194 gssdp_resource_group_set_client (resource_group,
195 g_value_get_object (value));
198 gssdp_resource_group_set_max_age (resource_group,
199 g_value_get_long (value));
202 gssdp_resource_group_set_available
203 (resource_group, g_value_get_boolean (value));
205 case PROP_MESSAGE_DELAY:
206 gssdp_resource_group_set_message_delay
207 (resource_group, g_value_get_uint (value));
210 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
216 gssdp_resource_group_dispose (GObject *object)
218 GSSDPResourceGroup *resource_group;
219 GSSDPResourceGroupPrivate *priv;
221 resource_group = GSSDP_RESOURCE_GROUP (object);
222 priv = resource_group->priv;
224 while (priv->resources) {
225 resource_free (priv->resources->data);
227 g_list_delete_link (priv->resources,
231 if (priv->message_queue) {
232 /* send messages without usual delay */
233 while (!g_queue_is_empty (priv->message_queue)) {
235 process_queue (resource_group);
237 g_free (g_queue_pop_head
238 (priv->message_queue));
241 g_queue_free (priv->message_queue);
242 priv->message_queue = NULL;
245 if (priv->message_src) {
246 g_source_destroy (priv->message_src);
247 priv->message_src = NULL;
250 if (priv->timeout_src) {
251 g_source_destroy (priv->timeout_src);
252 priv->timeout_src = NULL;
256 if (g_signal_handler_is_connected
258 priv->message_received_id)) {
259 g_signal_handler_disconnect
261 priv->message_received_id);
264 g_object_unref (priv->client);
268 G_OBJECT_CLASS (gssdp_resource_group_parent_class)->dispose (object);
272 gssdp_resource_group_class_init (GSSDPResourceGroupClass *klass)
274 GObjectClass *object_class;
276 object_class = G_OBJECT_CLASS (klass);
278 object_class->set_property = gssdp_resource_group_set_property;
279 object_class->get_property = gssdp_resource_group_get_property;
280 object_class->dispose = gssdp_resource_group_dispose;
282 g_type_class_add_private (klass, sizeof (GSSDPResourceGroupPrivate));
285 * GSSDPResourceGroup:client
287 * The #GSSDPClient to use.
289 g_object_class_install_property
295 "The associated client.",
297 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
298 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
299 G_PARAM_STATIC_BLURB));
302 * GSSDPResourceGroup:max-age
304 * The number of seconds our advertisements are valid.
306 g_object_class_install_property
312 "The number of seconds advertisements are valid.",
315 SSDP_DEFAULT_MAX_AGE,
317 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
318 G_PARAM_STATIC_BLURB));
321 * GSSDPResourceGroup:available
323 * Whether this group of resources is available or not.
325 g_object_class_install_property
331 "Whether this group of resources is available or "
335 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
336 G_PARAM_STATIC_BLURB));
339 * GSSDPResourceGroup:message-delay
341 * The minimum number of milliseconds between SSDP messages.
342 * The default is 20 based on DLNA specification.
344 g_object_class_install_property
350 "The minimum number of milliseconds between SSDP "
354 DEFAULT_MESSAGE_DELAY,
356 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
357 G_PARAM_STATIC_BLURB));
361 * gssdp_resource_group_new
362 * @client: The #GSSDPClient to associate with
364 * Return value: A new #GSSDPResourceGroup object.
367 gssdp_resource_group_new (GSSDPClient *client)
369 return g_object_new (GSSDP_TYPE_RESOURCE_GROUP,
375 * Sets the #GSSDPClient @resource_group is associated with @client
378 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
381 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
382 g_return_if_fail (GSSDP_IS_CLIENT (client));
384 resource_group->priv->client = g_object_ref (client);
386 resource_group->priv->message_received_id =
387 g_signal_connect_object (resource_group->priv->client,
389 G_CALLBACK (message_received_cb),
393 g_object_notify (G_OBJECT (resource_group), "client");
397 * gssdp_resource_group_get_client
398 * @resource_group: A #GSSDPResourceGroup
400 * Return value: The #GSSDPClient @resource_group is associated with.
403 gssdp_resource_group_get_client (GSSDPResourceGroup *resource_group)
405 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), NULL);
407 return resource_group->priv->client;
411 * gssdp_resource_group_set_max_age
412 * @resource_group: A #GSSDPResourceGroup
413 * @max_age: The number of seconds advertisements are valid
415 * Sets the number of seconds advertisements are valid to @max_age.
418 gssdp_resource_group_set_max_age (GSSDPResourceGroup *resource_group,
421 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
423 if (resource_group->priv->max_age == max_age)
426 resource_group->priv->max_age = max_age;
428 g_object_notify (G_OBJECT (resource_group), "max-age");
432 * gssdp_resource_group_get_max_age
433 * @resource_group: A #GSSDPResourceGroup
435 * Return value: The number of seconds advertisements are valid.
438 gssdp_resource_group_get_max_age (GSSDPResourceGroup *resource_group)
440 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
442 return resource_group->priv->max_age;
446 * gssdp_resource_group_set_message_delay
447 * @resource_group: A #GSSDPResourceGroup
448 * @message_delay: The message delay in ms.
450 * Sets the minimum time between each SSDP message.
453 gssdp_resource_group_set_message_delay (GSSDPResourceGroup *resource_group,
456 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
458 if (resource_group->priv->message_delay == message_delay)
461 resource_group->priv->message_delay = message_delay;
463 g_object_notify (G_OBJECT (resource_group), "message-delay");
467 * gssdp_resource_group_get_message_delay
468 * @resource_group: A #GSSDPResourceGroup
470 * Return value: the minimum time between each SSDP message in ms.
473 gssdp_resource_group_get_message_delay (GSSDPResourceGroup *resource_group)
475 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
477 return resource_group->priv->message_delay;
481 * gssdp_resource_group_set_available
482 * @resource_group: A #GSSDPResourceGroup
483 * @available: TRUE if @resource_group should be available (advertised)
485 * Sets @resource_group<!-- -->s availability to @available. Changing
486 * @resource_group<!-- -->s availability causes it to announce its new state
487 * to listening SSDP clients.
490 gssdp_resource_group_set_available (GSSDPResourceGroup *resource_group,
495 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
497 if (resource_group->priv->available == available)
500 resource_group->priv->available = available;
503 GMainContext *context;
506 /* We want to re-announce before actually timing out */
507 timeout = resource_group->priv->max_age;
509 timeout = timeout / 2 - 1;
511 /* Add re-announcement timer */
512 resource_group->priv->timeout_src =
513 g_timeout_source_new_seconds (timeout);
514 g_source_set_callback (resource_group->priv->timeout_src,
515 resource_group_timeout,
516 resource_group, NULL);
518 context = gssdp_client_get_main_context
519 (resource_group->priv->client);
520 g_source_attach (resource_group->priv->timeout_src, context);
522 g_source_unref (resource_group->priv->timeout_src);
524 /* Announce all resources */
525 for (l = resource_group->priv->resources; l; l = l->next)
526 resource_alive (l->data);
528 /* Unannounce all resources */
529 for (l = resource_group->priv->resources; l; l = l->next)
530 resource_byebye (l->data);
532 /* Remove re-announcement timer */
533 g_source_destroy (resource_group->priv->timeout_src);
534 resource_group->priv->timeout_src = NULL;
537 g_object_notify (G_OBJECT (resource_group), "available");
541 * gssdp_resource_group_get_available
542 * @resource_group: A #GSSDPResourceGroup
544 * Return value: TRUE if @resource_group is available (advertised).
547 gssdp_resource_group_get_available (GSSDPResourceGroup *resource_group)
549 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), FALSE);
551 return resource_group->priv->available;
555 * gssdp_resource_group_add_resource
556 * @resource_group: An @GSSDPResourceGroup
557 * @target: The resource's target
558 * @usn: The resource's USN
559 * @locations: A #GList of the resource's locations
561 * Adds a resource with target @target, USN @usn, and locations @locations
562 * to @resource_group.
564 * Return value: The ID of the added resource.
567 gssdp_resource_group_add_resource (GSSDPResourceGroup *resource_group,
576 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
577 g_return_val_if_fail (target != NULL, 0);
578 g_return_val_if_fail (usn != NULL, 0);
579 g_return_val_if_fail (locations != NULL, 0);
581 resource = g_slice_new0 (Resource);
583 resource->resource_group = resource_group;
585 resource->target = g_strdup (target);
586 resource->usn = g_strdup (usn);
589 resource->target_regex = create_target_regex (target, &error);
591 g_warning ("Error compiling regular expression for '%s': %s",
595 g_error_free (error);
596 resource_free (resource);
601 resource->initial_alive_sent = FALSE;
603 for (l = locations; l; l = l->next) {
604 resource->locations = g_list_append (resource->locations,
608 resource_group->priv->resources =
609 g_list_prepend (resource_group->priv->resources, resource);
611 resource->id = ++resource_group->priv->last_resource_id;
613 if (resource_group->priv->available)
614 resource_alive (resource);
620 * gssdp_resource_group_add_resource_simple
621 * @resource_group: An @GSSDPResourceGroup
622 * @target: The resource's target
623 * @usn: The resource's USN
624 * @location: The resource's location
626 * Adds a resource with target @target, USN @usn, and location @location
627 * to @resource_group.
629 * Return value: The ID of the added resource.
632 gssdp_resource_group_add_resource_simple (GSSDPResourceGroup *resource_group,
635 const char *location)
640 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
641 g_return_val_if_fail (target != NULL, 0);
642 g_return_val_if_fail (usn != NULL, 0);
643 g_return_val_if_fail (location != NULL, 0);
645 resource = g_slice_new0 (Resource);
647 resource->resource_group = resource_group;
649 resource->target = g_strdup (target);
650 resource->usn = g_strdup (usn);
653 resource->target_regex = create_target_regex (target, &error);
655 g_warning ("Error compiling regular expression for '%s': %s",
659 g_error_free (error);
660 resource_free (resource);
665 resource->locations = g_list_append (resource->locations,
666 g_strdup (location));
668 resource_group->priv->resources =
669 g_list_prepend (resource_group->priv->resources, resource);
671 resource->id = ++resource_group->priv->last_resource_id;
673 if (resource_group->priv->available)
674 resource_alive (resource);
680 * gssdp_resource_group_remove_resource
681 * @resource_group: An @GSSDPResourceGroup
682 * @resource_id: The ID of the resource to remove
684 * Removes the resource with ID @resource_id from @resource_group.
687 gssdp_resource_group_remove_resource (GSSDPResourceGroup *resource_group,
692 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
693 g_return_if_fail (resource_id > 0);
695 for (l = resource_group->priv->resources; l; l = l->next) {
700 if (resource->id == resource_id) {
701 resource_group->priv->resources =
702 g_list_remove (resource_group->priv->resources,
705 resource_free (resource);
713 * Called to re-announce all resources periodically
716 resource_group_timeout (gpointer user_data)
718 GSSDPResourceGroup *resource_group;
721 resource_group = GSSDP_RESOURCE_GROUP (user_data);
723 /* Re-announce all resources */
724 for (l = resource_group->priv->resources; l; l = l->next)
725 resource_alive (l->data);
734 message_received_cb (GSSDPClient *client,
737 _GSSDPMessageType type,
738 SoupMessageHeaders *headers,
741 GSSDPResourceGroup *resource_group;
742 const char *target, *mx_str;
747 resource_group = GSSDP_RESOURCE_GROUP (user_data);
749 /* Only process if we are available */
750 if (!resource_group->priv->available)
753 /* We only handle discovery requests */
754 if (type != _GSSDP_DISCOVERY_REQUEST)
758 target = soup_message_headers_get_one (headers, "ST");
760 g_warning ("Discovery request did not have an ST header");
765 /* Is this the "ssdp:all" target? */
766 want_all = (strcmp (target, GSSDP_ALL_RESOURCES) == 0);
769 mx_str = soup_message_headers_get_one (headers, "MX");
773 mx = SSDP_DEFAULT_MX;
775 /* Find matching resource */
776 for (l = resource_group->priv->resources; l; l = l->next) {
782 g_regex_match (resource->target_regex,
788 DiscoveryResponse *response;
789 GMainContext *context;
791 /* Get a random timeout from the interval [0, mx] */
792 timeout = g_random_int_range (0, mx * 1000);
794 /* Prepare response */
795 response = g_slice_new (DiscoveryResponse);
797 response->dest_ip = g_strdup (from_ip);
798 response->dest_port = from_port;
799 response->resource = resource;
802 response->target = g_strdup (resource->target);
804 response->target = g_strdup (target);
807 response->timeout_src = g_timeout_source_new (timeout);
808 g_source_set_callback (response->timeout_src,
809 discovery_response_timeout,
812 context = gssdp_client_get_main_context (client);
813 g_source_attach (response->timeout_src, context);
815 g_source_unref (response->timeout_src);
817 /* Add to resource */
818 resource->responses =
819 g_list_prepend (resource->responses, response);
825 * Construct the AL (Alternative Locations) header for @resource
828 construct_al (Resource *resource)
830 if (resource->locations->next) {
834 al_string = g_string_new ("AL: ");
836 for (l = resource->locations->next; l; l = l->next) {
837 g_string_append_c (al_string, '<');
838 g_string_append (al_string, l->data);
839 g_string_append_c (al_string, '>');
842 g_string_append (al_string, "\r\n");
844 return g_string_free (al_string, FALSE);
850 * Send a discovery response
853 discovery_response_timeout (gpointer user_data)
855 DiscoveryResponse *response;
858 char *al, *date_str, *message;
861 response = user_data;
864 client = response->resource->resource_group->priv->client;
866 max_age = response->resource->resource_group->priv->max_age;
868 al = construct_al (response->resource);
870 date = soup_date_new_from_now (0);
871 date_str = soup_date_to_string (date, SOUP_DATE_HTTP);
872 soup_date_free (date);
874 message = g_strdup_printf (SSDP_DISCOVERY_RESPONSE,
875 (char *) response->resource->locations->data,
877 response->resource->usn,
878 gssdp_client_get_server_id (client),
883 _gssdp_client_send_message (client,
892 discovery_response_free (response);
898 * Free a DiscoveryResponse structure and its contained data
901 discovery_response_free (DiscoveryResponse *response)
903 response->resource->responses =
904 g_list_remove (response->resource->responses, response);
906 g_source_destroy (response->timeout_src);
908 g_free (response->dest_ip);
909 g_free (response->target);
911 g_slice_free (DiscoveryResponse, response);
915 * Send the next queued message, if any
918 process_queue (gpointer data)
920 GSSDPResourceGroup *resource_group;
922 resource_group = GSSDP_RESOURCE_GROUP (data);
924 if (g_queue_is_empty (resource_group->priv->message_queue)) {
925 /* this is the timeout after last message in queue */
926 resource_group->priv->message_src = NULL;
933 client = resource_group->priv->client;
934 message = g_queue_pop_head
935 (resource_group->priv->message_queue);
937 _gssdp_client_send_message (client,
948 * Add a message to sending queue
950 * Do not free @message.
953 queue_message (GSSDPResourceGroup *resource_group,
956 g_queue_push_tail (resource_group->priv->message_queue,
959 if (resource_group->priv->message_src == NULL) {
960 /* nothing in the queue: process message immediately
961 and add a timeout for (possible) next message */
962 GMainContext *context;
964 process_queue (resource_group);
965 resource_group->priv->message_src = g_timeout_source_new (
966 resource_group->priv->message_delay);
967 g_source_set_callback (resource_group->priv->message_src,
968 process_queue, resource_group, NULL);
969 context = gssdp_client_get_main_context (
970 resource_group->priv->client);
971 g_source_attach (resource_group->priv->message_src, context);
972 g_source_unref (resource_group->priv->message_src);
977 * Send ssdp:alive message for @resource
980 resource_alive (Resource *resource)
986 if (!resource->initial_alive_sent) {
987 /* Unannounce before first announce. This is done to
988 minimize the possibility of control points thinking
989 that this is just a reannouncement. */
990 resource_byebye (resource);
992 resource->initial_alive_sent = TRUE;
996 client = resource->resource_group->priv->client;
998 max_age = resource->resource_group->priv->max_age;
1000 al = construct_al (resource);
1002 message = g_strdup_printf (SSDP_ALIVE_MESSAGE,
1004 (char *) resource->locations->data,
1006 gssdp_client_get_server_id (client),
1010 queue_message (resource->resource_group, message);
1016 * Send ssdp:byebye message for @resource
1019 resource_byebye (Resource *resource)
1024 message = g_strdup_printf (SSDP_BYEBYE_MESSAGE,
1028 queue_message (resource->resource_group, message);
1032 * Free a Resource structure and its contained data
1035 resource_free (Resource *resource)
1037 while (resource->responses)
1038 discovery_response_free (resource->responses->data);
1040 if (resource->resource_group->priv->available)
1041 resource_byebye (resource);
1043 g_free (resource->usn);
1044 g_free (resource->target);
1046 if (resource->target_regex)
1047 g_regex_unref (resource->target_regex);
1049 while (resource->locations) {
1050 g_free (resource->locations->data);
1051 resource->locations = g_list_delete_link (resource->locations,
1052 resource->locations);
1055 g_slice_free (Resource, resource);
1059 create_target_regex (const char *target, GError **error)
1064 char *version_pattern;
1066 if (strncmp (target, "urn:", 4) != 0) {
1067 /* target is not a URN, No need to deal with version. */
1068 return g_regex_new (target, 0, 0, error);
1071 version_pattern = "[0-9]+$";
1072 /* Make sure we have enough room for version pattern */
1073 pattern = g_strndup (target,
1074 strlen (target) + strlen (version_pattern));
1076 version = g_strrstr (pattern, ":") + 1;
1077 if (version != NULL &&
1078 g_regex_match_simple (version_pattern, version, 0, 0)) {
1079 strcpy (version, version_pattern);
1082 regex = g_regex_new (pattern, 0, 0, error);