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);
270 gssdp_resource_group_class_init (GSSDPResourceGroupClass *klass)
272 GObjectClass *object_class;
274 object_class = G_OBJECT_CLASS (klass);
276 object_class->set_property = gssdp_resource_group_set_property;
277 object_class->get_property = gssdp_resource_group_get_property;
278 object_class->dispose = gssdp_resource_group_dispose;
280 g_type_class_add_private (klass, sizeof (GSSDPResourceGroupPrivate));
283 * GSSDPResourceGroup:client
285 * The #GSSDPClient to use.
287 g_object_class_install_property
293 "The associated client.",
295 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
296 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
297 G_PARAM_STATIC_BLURB));
300 * GSSDPResourceGroup:max-age
302 * The number of seconds our advertisements are valid.
304 g_object_class_install_property
310 "The number of seconds advertisements are valid.",
313 SSDP_DEFAULT_MAX_AGE,
315 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
316 G_PARAM_STATIC_BLURB));
319 * GSSDPResourceGroup:available
321 * Whether this group of resources is available or not.
323 g_object_class_install_property
329 "Whether this group of resources is available or "
333 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
334 G_PARAM_STATIC_BLURB));
337 * GSSDPResourceGroup:message-delay
339 * The minimum number of milliseconds between SSDP messages.
340 * The default is 20 based on DLNA specification.
342 g_object_class_install_property
348 "The minimum number of milliseconds between SSDP "
352 DEFAULT_MESSAGE_DELAY,
354 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
355 G_PARAM_STATIC_BLURB));
359 * gssdp_resource_group_new
360 * @client: The #GSSDPClient to associate with
362 * Return value: A new #GSSDPResourceGroup object.
365 gssdp_resource_group_new (GSSDPClient *client)
367 return g_object_new (GSSDP_TYPE_RESOURCE_GROUP,
373 * Sets the #GSSDPClient @resource_group is associated with @client
376 gssdp_resource_group_set_client (GSSDPResourceGroup *resource_group,
379 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
380 g_return_if_fail (GSSDP_IS_CLIENT (client));
382 resource_group->priv->client = g_object_ref (client);
384 resource_group->priv->message_received_id =
385 g_signal_connect_object (resource_group->priv->client,
387 G_CALLBACK (message_received_cb),
391 g_object_notify (G_OBJECT (resource_group), "client");
395 * gssdp_resource_group_get_client
396 * @resource_group: A #GSSDPResourceGroup
398 * Return value: The #GSSDPClient @resource_group is associated with.
401 gssdp_resource_group_get_client (GSSDPResourceGroup *resource_group)
403 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), NULL);
405 return resource_group->priv->client;
409 * gssdp_resource_group_set_max_age
410 * @resource_group: A #GSSDPResourceGroup
411 * @max_age: The number of seconds advertisements are valid
413 * Sets the number of seconds advertisements are valid to @max_age.
416 gssdp_resource_group_set_max_age (GSSDPResourceGroup *resource_group,
419 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
421 if (resource_group->priv->max_age == max_age)
424 resource_group->priv->max_age = max_age;
426 g_object_notify (G_OBJECT (resource_group), "max-age");
430 * gssdp_resource_group_get_max_age
431 * @resource_group: A #GSSDPResourceGroup
433 * Return value: The number of seconds advertisements are valid.
436 gssdp_resource_group_get_max_age (GSSDPResourceGroup *resource_group)
438 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
440 return resource_group->priv->max_age;
444 * gssdp_resource_group_set_message_delay
445 * @resource_group: A #GSSDPResourceGroup
446 * @message_delay: The message delay in ms.
448 * Sets the minimum time between each SSDP message.
451 gssdp_resource_group_set_message_delay (GSSDPResourceGroup *resource_group,
454 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
456 if (resource_group->priv->message_delay == message_delay)
459 resource_group->priv->message_delay = message_delay;
461 g_object_notify (G_OBJECT (resource_group), "message-delay");
465 * gssdp_resource_group_get_message_delay
466 * @resource_group: A #GSSDPResourceGroup
468 * Return value: the minimum time between each SSDP message in ms.
471 gssdp_resource_group_get_message_delay (GSSDPResourceGroup *resource_group)
473 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
475 return resource_group->priv->message_delay;
479 * gssdp_resource_group_set_available
480 * @resource_group: A #GSSDPResourceGroup
481 * @available: TRUE if @resource_group should be available (advertised)
483 * Sets @resource_group<!-- -->s availability to @available. Changing
484 * @resource_group<!-- -->s availability causes it to announce its new state
485 * to listening SSDP clients.
488 gssdp_resource_group_set_available (GSSDPResourceGroup *resource_group,
493 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
495 if (resource_group->priv->available == available)
498 resource_group->priv->available = available;
501 GMainContext *context;
504 /* We want to re-announce before actually timing out */
505 timeout = resource_group->priv->max_age;
507 timeout = timeout / 2 - 1;
509 /* Add re-announcement timer */
510 resource_group->priv->timeout_src =
511 g_timeout_source_new_seconds (timeout);
512 g_source_set_callback (resource_group->priv->timeout_src,
513 resource_group_timeout,
514 resource_group, NULL);
516 context = gssdp_client_get_main_context
517 (resource_group->priv->client);
518 g_source_attach (resource_group->priv->timeout_src, context);
520 g_source_unref (resource_group->priv->timeout_src);
522 /* Announce all resources */
523 for (l = resource_group->priv->resources; l; l = l->next)
524 resource_alive (l->data);
526 /* Unannounce all resources */
527 for (l = resource_group->priv->resources; l; l = l->next)
528 resource_byebye (l->data);
530 /* Remove re-announcement timer */
531 g_source_destroy (resource_group->priv->timeout_src);
532 resource_group->priv->timeout_src = NULL;
535 g_object_notify (G_OBJECT (resource_group), "available");
539 * gssdp_resource_group_get_available
540 * @resource_group: A #GSSDPResourceGroup
542 * Return value: TRUE if @resource_group is available (advertised).
545 gssdp_resource_group_get_available (GSSDPResourceGroup *resource_group)
547 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), FALSE);
549 return resource_group->priv->available;
553 * gssdp_resource_group_add_resource
554 * @resource_group: An @GSSDPResourceGroup
555 * @target: The resource's target
556 * @usn: The resource's USN
557 * @locations: A #GList of the resource's locations
559 * Adds a resource with target @target, USN @usn, and locations @locations
560 * to @resource_group.
562 * Return value: The ID of the added resource.
565 gssdp_resource_group_add_resource (GSSDPResourceGroup *resource_group,
574 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
575 g_return_val_if_fail (target != NULL, 0);
576 g_return_val_if_fail (usn != NULL, 0);
577 g_return_val_if_fail (locations != NULL, 0);
579 resource = g_slice_new0 (Resource);
581 resource->resource_group = resource_group;
583 resource->target = g_strdup (target);
584 resource->usn = g_strdup (usn);
587 resource->target_regex = create_target_regex (target, &error);
589 g_warning ("Error compiling regular expression for '%s': %s",
593 g_error_free (error);
594 resource_free (resource);
599 resource->initial_alive_sent = FALSE;
601 for (l = locations; l; l = l->next) {
602 resource->locations = g_list_append (resource->locations,
606 resource_group->priv->resources =
607 g_list_prepend (resource_group->priv->resources, resource);
609 resource->id = ++resource_group->priv->last_resource_id;
611 if (resource_group->priv->available)
612 resource_alive (resource);
618 * gssdp_resource_group_add_resource_simple
619 * @resource_group: An @GSSDPResourceGroup
620 * @target: The resource's target
621 * @usn: The resource's USN
622 * @location: The resource's location
624 * Adds a resource with target @target, USN @usn, and location @location
625 * to @resource_group.
627 * Return value: The ID of the added resource.
630 gssdp_resource_group_add_resource_simple (GSSDPResourceGroup *resource_group,
633 const char *location)
638 g_return_val_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group), 0);
639 g_return_val_if_fail (target != NULL, 0);
640 g_return_val_if_fail (usn != NULL, 0);
641 g_return_val_if_fail (location != NULL, 0);
643 resource = g_slice_new0 (Resource);
645 resource->resource_group = resource_group;
647 resource->target = g_strdup (target);
648 resource->usn = g_strdup (usn);
651 resource->target_regex = create_target_regex (target, &error);
653 g_warning ("Error compiling regular expression for '%s': %s",
657 g_error_free (error);
658 resource_free (resource);
663 resource->locations = g_list_append (resource->locations,
664 g_strdup (location));
666 resource_group->priv->resources =
667 g_list_prepend (resource_group->priv->resources, resource);
669 resource->id = ++resource_group->priv->last_resource_id;
671 if (resource_group->priv->available)
672 resource_alive (resource);
678 * gssdp_resource_group_remove_resource
679 * @resource_group: An @GSSDPResourceGroup
680 * @resource_id: The ID of the resource to remove
682 * Removes the resource with ID @resource_id from @resource_group.
685 gssdp_resource_group_remove_resource (GSSDPResourceGroup *resource_group,
690 g_return_if_fail (GSSDP_IS_RESOURCE_GROUP (resource_group));
691 g_return_if_fail (resource_id > 0);
693 for (l = resource_group->priv->resources; l; l = l->next) {
698 if (resource->id == resource_id) {
699 resource_group->priv->resources =
700 g_list_remove (resource_group->priv->resources,
703 resource_free (resource);
711 * Called to re-announce all resources periodically
714 resource_group_timeout (gpointer user_data)
716 GSSDPResourceGroup *resource_group;
719 resource_group = GSSDP_RESOURCE_GROUP (user_data);
721 /* Re-announce all resources */
722 for (l = resource_group->priv->resources; l; l = l->next)
723 resource_alive (l->data);
732 message_received_cb (GSSDPClient *client,
735 _GSSDPMessageType type,
736 SoupMessageHeaders *headers,
739 GSSDPResourceGroup *resource_group;
740 const char *target, *mx_str;
745 resource_group = GSSDP_RESOURCE_GROUP (user_data);
747 /* Only process if we are available */
748 if (!resource_group->priv->available)
751 /* We only handle discovery requests */
752 if (type != _GSSDP_DISCOVERY_REQUEST)
756 target = soup_message_headers_get (headers, "ST");
758 g_warning ("Discovery request did not have an ST header");
763 /* Is this the "ssdp:all" target? */
764 want_all = (strcmp (target, GSSDP_ALL_RESOURCES) == 0);
767 mx_str = soup_message_headers_get (headers, "MX");
771 mx = SSDP_DEFAULT_MX;
773 /* Find matching resource */
774 for (l = resource_group->priv->resources; l; l = l->next) {
780 g_regex_match (resource->target_regex,
786 DiscoveryResponse *response;
787 GMainContext *context;
789 /* Get a random timeout from the interval [0, mx] */
790 timeout = g_random_int_range (0, mx * 1000);
792 /* Prepare response */
793 response = g_slice_new (DiscoveryResponse);
795 response->dest_ip = g_strdup (from_ip);
796 response->dest_port = from_port;
797 response->resource = resource;
800 response->target = g_strdup (resource->target);
802 response->target = g_strdup (target);
805 response->timeout_src = g_timeout_source_new (timeout);
806 g_source_set_callback (response->timeout_src,
807 discovery_response_timeout,
810 context = gssdp_client_get_main_context (client);
811 g_source_attach (response->timeout_src, context);
813 g_source_unref (response->timeout_src);
815 /* Add to resource */
816 resource->responses =
817 g_list_prepend (resource->responses, response);
823 * Construct the AL (Alternative Locations) header for @resource
826 construct_al (Resource *resource)
828 if (resource->locations->next) {
832 al_string = g_string_new ("AL: ");
834 for (l = resource->locations->next; l; l = l->next) {
835 g_string_append_c (al_string, '<');
836 g_string_append (al_string, l->data);
837 g_string_append_c (al_string, '>');
840 g_string_append (al_string, "\r\n");
842 return g_string_free (al_string, FALSE);
848 * Send a discovery response
851 discovery_response_timeout (gpointer user_data)
853 DiscoveryResponse *response;
856 char *al, *date_str, *message;
859 response = user_data;
862 client = response->resource->resource_group->priv->client;
864 max_age = response->resource->resource_group->priv->max_age;
866 al = construct_al (response->resource);
868 date = soup_date_new_from_now (0);
869 date_str = soup_date_to_string (date, SOUP_DATE_HTTP);
870 soup_date_free (date);
872 message = g_strdup_printf (SSDP_DISCOVERY_RESPONSE,
873 (char *) response->resource->locations->data,
875 response->resource->usn,
876 gssdp_client_get_server_id (client),
881 _gssdp_client_send_message (client,
890 discovery_response_free (response);
896 * Free a DiscoveryResponse structure and its contained data
899 discovery_response_free (DiscoveryResponse *response)
901 response->resource->responses =
902 g_list_remove (response->resource->responses, response);
904 g_source_destroy (response->timeout_src);
906 g_free (response->dest_ip);
907 g_free (response->target);
909 g_slice_free (DiscoveryResponse, response);
913 * Send the next queued message, if any
916 process_queue (gpointer data)
918 GSSDPResourceGroup *resource_group;
920 resource_group = GSSDP_RESOURCE_GROUP (data);
922 if (g_queue_is_empty (resource_group->priv->message_queue)) {
923 /* this is the timeout after last message in queue */
924 resource_group->priv->message_src = NULL;
931 client = resource_group->priv->client;
932 message = g_queue_pop_head
933 (resource_group->priv->message_queue);
935 _gssdp_client_send_message (client,
946 * Add a message to sending queue
948 * Do not free @message.
951 queue_message (GSSDPResourceGroup *resource_group,
954 g_queue_push_tail (resource_group->priv->message_queue,
957 if (resource_group->priv->message_src == NULL) {
958 /* nothing in the queue: process message immediately
959 and add a timeout for (possible) next message */
960 GMainContext *context;
962 process_queue (resource_group);
963 resource_group->priv->message_src = g_timeout_source_new (
964 resource_group->priv->message_delay);
965 g_source_set_callback (resource_group->priv->message_src,
966 process_queue, resource_group, NULL);
967 context = gssdp_client_get_main_context (
968 resource_group->priv->client);
969 g_source_attach (resource_group->priv->message_src, context);
970 g_source_unref (resource_group->priv->message_src);
975 * Send ssdp:alive message for @resource
978 resource_alive (Resource *resource)
984 if (!resource->initial_alive_sent) {
985 /* Unannounce before first announce. This is done to
986 minimize the possibility of control points thinking
987 that this is just a reannouncement. */
988 resource_byebye (resource);
990 resource->initial_alive_sent = TRUE;
994 client = resource->resource_group->priv->client;
996 max_age = resource->resource_group->priv->max_age;
998 al = construct_al (resource);
1000 message = g_strdup_printf (SSDP_ALIVE_MESSAGE,
1002 (char *) resource->locations->data,
1004 gssdp_client_get_server_id (client),
1008 queue_message (resource->resource_group, message);
1014 * Send ssdp:byebye message for @resource
1017 resource_byebye (Resource *resource)
1022 message = g_strdup_printf (SSDP_BYEBYE_MESSAGE,
1026 queue_message (resource->resource_group, message);
1030 * Free a Resource structure and its contained data
1033 resource_free (Resource *resource)
1035 while (resource->responses)
1036 discovery_response_free (resource->responses->data);
1038 if (resource->resource_group->priv->available)
1039 resource_byebye (resource);
1041 g_free (resource->usn);
1042 g_free (resource->target);
1044 if (resource->target_regex)
1045 g_regex_unref (resource->target_regex);
1047 while (resource->locations) {
1048 g_free (resource->locations->data);
1049 resource->locations = g_list_delete_link (resource->locations,
1050 resource->locations);
1053 g_slice_free (Resource, resource);
1057 create_target_regex (const char *target, GError **error)
1062 char *version_pattern;
1064 version_pattern = "[0-9]+$";
1065 /* Make sure we have enough room for version pattern */
1066 pattern = g_strndup (target,
1067 strlen (target) + strlen (version_pattern));
1069 version = g_strrstr (pattern, ":") + 1;
1070 if (version != NULL &&
1071 g_regex_match_simple (version_pattern, version, 0, 0)) {
1072 strcpy (version, version_pattern);
1075 regex = g_regex_new (pattern, 0, 0, error);