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:gupnp-control-point
24 * @short_description: Class for resource discovery.
26 * #GUPnPControlPoint handles device and service discovery. After creating
27 * a control point and activating it using gssdp_resource_browser_set_active(),
28 * the ::device-proxy-available, ::service-proxy-available,
29 * ::device-proxy-unavailable and ::service-proxy-unavailable signals will
30 * be emitted whenever the availability of a device or service matching
31 * the specified discovery target changes.
36 #include "gupnp-control-point.h"
37 #include "gupnp-context-private.h"
38 #include "gupnp-resource-factory-private.h"
39 #include "http-headers.h"
42 G_DEFINE_TYPE (GUPnPControlPoint,
44 GSSDP_TYPE_RESOURCE_BROWSER);
46 struct _GUPnPControlPointPrivate {
47 GUPnPResourceFactory *factory;
52 GHashTable *doc_cache;
59 PROP_RESOURCE_FACTORY,
63 DEVICE_PROXY_AVAILABLE,
64 DEVICE_PROXY_UNAVAILABLE,
65 SERVICE_PROXY_AVAILABLE,
66 SERVICE_PROXY_UNAVAILABLE,
70 static guint signals[SIGNAL_LAST];
73 GUPnPControlPoint *control_point;
77 char *description_url;
80 } GetDescriptionURLData;
83 get_description_url_data_free (GetDescriptionURLData *data)
85 data->control_point->priv->pending_gets =
86 g_list_remove (data->control_point->priv->pending_gets, data);
89 g_free (data->service_type);
90 g_free (data->description_url);
92 g_slice_free (GetDescriptionURLData, data);
96 gupnp_control_point_init (GUPnPControlPoint *control_point)
99 G_TYPE_INSTANCE_GET_PRIVATE (control_point,
100 GUPNP_TYPE_CONTROL_POINT,
101 GUPnPControlPointPrivate);
103 control_point->priv->doc_cache =
104 g_hash_table_new_full (g_str_hash,
110 /* Return TRUE if value == user_data */
112 find_doc (G_GNUC_UNUSED gpointer key,
114 G_GNUC_UNUSED gpointer user_data)
116 return (value == user_data);
119 /* xmlDoc wrapper finalized */
121 doc_finalized (gpointer user_data,
122 GObject *where_the_object_was)
124 GUPnPControlPoint *control_point;
126 control_point = GUPNP_CONTROL_POINT (user_data);
128 g_hash_table_foreach_remove (control_point->priv->doc_cache,
130 where_the_object_was);
133 /* Release weak reference on xmlDoc wrapper */
135 weak_unref_doc (G_GNUC_UNUSED gpointer key,
139 g_object_weak_unref (G_OBJECT (value), doc_finalized, user_data);
143 gupnp_control_point_dispose (GObject *object)
145 GUPnPControlPoint *control_point;
146 GSSDPResourceBrowser *browser;
147 GObjectClass *object_class;
149 control_point = GUPNP_CONTROL_POINT (object);
150 browser = GSSDP_RESOURCE_BROWSER (control_point);
152 gssdp_resource_browser_set_active (browser, FALSE);
154 if (control_point->priv->factory) {
155 g_object_unref (control_point->priv->factory);
156 control_point->priv->factory = NULL;
159 /* Cancel any pending description file GETs */
160 while (control_point->priv->pending_gets) {
161 GetDescriptionURLData *data;
162 GUPnPContext *context;
163 SoupSession *session;
165 data = control_point->priv->pending_gets->data;
167 context = gupnp_control_point_get_context (control_point);
168 session = gupnp_context_get_session (context);
170 soup_session_cancel_message (session,
172 SOUP_STATUS_CANCELLED);
174 get_description_url_data_free (data);
177 /* Release weak references on remaining cached documents */
178 g_hash_table_foreach (control_point->priv->doc_cache,
183 object_class = G_OBJECT_CLASS (gupnp_control_point_parent_class);
184 object_class->dispose (object);
188 gupnp_control_point_finalize (GObject *object)
190 GUPnPControlPoint *control_point;
191 GObjectClass *object_class;
193 control_point = GUPNP_CONTROL_POINT (object);
195 g_hash_table_destroy (control_point->priv->doc_cache);
198 object_class = G_OBJECT_CLASS (gupnp_control_point_parent_class);
199 object_class->finalize (object);
203 find_service_node (GUPnPControlPoint *control_point,
205 const char *service_type)
209 l = control_point->priv->services;
212 GUPnPServiceInfo *info;
214 info = GUPNP_SERVICE_INFO (l->data);
216 if ((strcmp (gupnp_service_info_get_udn (info), udn) == 0) &&
217 (strcmp (gupnp_service_info_get_service_type (info),
228 find_device_node (GUPnPControlPoint *control_point,
233 l = control_point->priv->devices;
236 GUPnPDeviceInfo *info;
238 info = GUPNP_DEVICE_INFO (l->data);
240 if (strcmp (udn, gupnp_device_info_get_udn (info)) == 0)
250 create_and_report_service_proxy (GUPnPControlPoint *control_point,
254 const char *service_type,
255 const char *description_url,
258 GUPnPServiceProxy *proxy;
259 GUPnPResourceFactory *factory;
260 GUPnPContext *context;
262 if (find_service_node (control_point, udn, service_type) != NULL)
263 /* We already have a proxy for this service */
266 factory = gupnp_control_point_get_resource_factory (control_point);
267 context = gupnp_control_point_get_context (control_point);
270 proxy = gupnp_resource_factory_create_service_proxy (factory,
279 control_point->priv->services =
280 g_list_prepend (control_point->priv->services,
283 g_signal_emit (control_point,
284 signals[SERVICE_PROXY_AVAILABLE],
290 create_and_report_device_proxy (GUPnPControlPoint *control_point,
294 const char *description_url,
297 GUPnPDeviceProxy *proxy;
298 GUPnPResourceFactory *factory;
299 GUPnPContext *context;
301 if (find_device_node (control_point, udn) != NULL)
302 /* We already have a proxy for this device */
305 factory = gupnp_control_point_get_resource_factory (control_point);
306 context = gupnp_control_point_get_context (control_point);
308 proxy = gupnp_resource_factory_create_device_proxy (factory,
316 control_point->priv->devices = g_list_prepend
317 (control_point->priv->devices,
320 g_signal_emit (control_point,
321 signals[DEVICE_PROXY_AVAILABLE],
327 compare_service_types_versioned (const char *searched_service,
328 const char *current_service)
330 const char *searched_version_ptr, *current_version_ptr;
331 guint searched_version, current_version, searched_length;
332 guint current_length;
334 searched_version_ptr = strrchr (searched_service, ':');
335 if (searched_version_ptr == NULL)
338 current_version_ptr = strrchr (current_service, ':');
339 if (current_version_ptr == NULL)
342 searched_length = (searched_version_ptr - searched_service);
343 current_length = (current_version_ptr - current_service);
345 if (searched_length != current_length)
348 searched_version = (guint) atol (searched_version_ptr + 1);
349 if (searched_version == 0)
352 current_version = (guint) atol (current_version_ptr + 1);
353 if (current_version == 0)
356 if (current_version < searched_version)
359 return strncmp (searched_service,
361 searched_length) == 0;
364 /* Search @element for matching services */
366 process_service_list (xmlNode *element,
367 GUPnPControlPoint *control_point,
370 const char *service_type,
371 const char *description_url,
374 g_object_ref (control_point);
376 for (element = element->children; element; element = element->next) {
380 if (strcmp ((char *) element->name, "service") != 0)
383 /* See if this is a matching service */
384 prop = xml_util_get_child_element_content (element,
389 match = compare_service_types_versioned (service_type,
398 create_and_report_service_proxy (control_point,
407 g_object_unref (control_point);
410 /* Recursively search @element for matching devices */
412 process_device_list (xmlNode *element,
413 GUPnPControlPoint *control_point,
416 const char *service_type,
417 const char *description_url,
420 g_object_ref (control_point);
422 for (element = element->children; element; element = element->next) {
427 if (strcmp ((char *) element->name, "device") != 0)
430 /* Recurse into children */
431 children = xml_util_get_element (element,
436 process_device_list (children,
445 /* See if this is a matching device */
446 prop = xml_util_get_child_element_content (element, "UDN");
450 match = (strcmp ((char *) prop, udn) == 0);
460 /* Dive into serviceList */
461 children = xml_util_get_element (element,
466 process_service_list (children,
475 create_and_report_device_proxy (control_point,
483 g_object_unref (control_point);
487 * Called when the description document is loaded.
490 description_loaded (GUPnPControlPoint *control_point,
493 const char *service_type,
494 const char *description_url)
499 /* Save the URL base, if any */
500 element = xml_util_get_element ((xmlNode *) doc->doc,
503 if (element == NULL) {
504 g_warning ("No 'root' element found in description document"
505 " '%s'. Ignoring device '%s'\n",
512 url_base = xml_util_get_child_element_content_uri (element,
516 url_base = soup_uri_new (description_url);
518 /* Iterate matching devices */
519 process_device_list (element,
528 soup_uri_free (url_base);
532 * Description URL downloaded.
535 got_description_url (G_GNUC_UNUSED SoupSession *session,
537 GetDescriptionURLData *data)
541 if (msg->status_code == SOUP_STATUS_CANCELLED)
544 /* Now, make sure again this document is not already cached. If it is,
545 * we re-use the cached one. */
546 doc = g_hash_table_lookup (data->control_point->priv->doc_cache,
547 data->description_url);
550 description_loaded (data->control_point,
554 data->description_url);
556 get_description_url_data_free (data);
562 if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
566 xml_doc = xmlRecoverMemory (msg->response_body->data,
567 msg->response_body->length);
569 doc = gupnp_xml_doc_new (xml_doc);
571 description_loaded (data->control_point,
575 data->description_url);
577 /* Insert into document cache */
579 (data->control_point->priv->doc_cache,
580 g_strdup (data->description_url),
583 /* Make sure the document is removed from the cache
585 g_object_weak_ref (G_OBJECT (doc),
587 data->control_point);
589 /* If no proxy was created, make sure doc is freed. */
590 g_object_unref (doc);
592 g_warning ("Failed to parse %s", data->description_url);
594 g_warning ("Failed to GET %s: %s",
595 data->description_url,
598 get_description_url_data_free (data);
602 * Downloads and parses (or takes from cache) @description_url,
604 * - A #GUPnPDeviceProxy for the device specified by @udn if @service_type
606 * - A #GUPnPServiceProxy for the service of type @service_type from the device
607 * specified by @udn if @service_type is not %NULL.
610 load_description (GUPnPControlPoint *control_point,
611 const char *description_url,
613 const char *service_type)
617 doc = g_hash_table_lookup (control_point->priv->doc_cache,
621 description_loaded (control_point,
627 /* Asynchronously download doc */
628 GUPnPContext *context;
629 SoupSession *session;
630 GetDescriptionURLData *data;
632 context = gupnp_control_point_get_context (control_point);
634 session = gupnp_context_get_session (context);
636 data = g_slice_new (GetDescriptionURLData);
638 data->message = soup_message_new (SOUP_METHOD_GET,
640 if (data->message == NULL) {
641 g_warning ("Invalid description URL: %s",
644 g_slice_free (GetDescriptionURLData, data);
649 http_request_set_accept_language (data->message);
651 data->control_point = control_point;
653 data->udn = g_strdup (udn);
654 data->service_type = g_strdup (service_type);
655 data->description_url = g_strdup (description_url);
657 control_point->priv->pending_gets =
658 g_list_prepend (control_point->priv->pending_gets,
661 soup_session_queue_message (session,
663 (SoupSessionCallback)
670 parse_usn (const char *usn,
680 *udn = *service_type = NULL;
682 /* Verify we have a valid USN */
683 if (strncmp (usn, "uuid:", strlen ("uuid:"))) {
684 g_warning ("Invalid USN: %s", usn);
690 bits = g_strsplit (usn, "::", -1);
693 count = g_strv_length (bits);
696 /* uuid:device-UUID */
702 } else if (count == 2) {
706 second_bits = g_strsplit (bits[1], ":", -1);
707 n_second_bits = g_strv_length (second_bits);
709 if (n_second_bits >= 2 &&
710 !strcmp (second_bits[0], "upnp") &&
711 !strcmp (second_bits[1], "rootdevice")) {
712 /* uuid:device-UUID::upnp:rootdevice */
717 } else if (n_second_bits >= 3 &&
718 !strcmp (second_bits[0], "urn")) {
719 /* uuid:device-UIID::urn:domain-name:service/device:
722 if (!strcmp (second_bits[2], "device")) {
726 } else if (!strcmp (second_bits[2], "service")) {
728 *service_type = bits[1];
734 g_strfreev (second_bits);
738 g_warning ("Invalid USN: %s", usn);
740 for (i = 0; i < count; i++) {
741 if ((bits[i] != *udn) &&
742 (bits[i] != *service_type))
752 gupnp_control_point_resource_available (GSSDPResourceBrowser *resource_browser,
754 const GList *locations)
756 GUPnPControlPoint *control_point;
757 char *udn, *service_type;
759 control_point = GUPNP_CONTROL_POINT (resource_browser);
761 /* Verify we have a location */
763 g_warning ("No Location header for device with USN %s", usn);
768 if (!parse_usn (usn, &udn, &service_type))
771 load_description (control_point,
777 g_free (service_type);
781 gupnp_control_point_resource_unavailable
782 (GSSDPResourceBrowser *resource_browser,
785 GUPnPControlPoint *control_point;
786 char *udn, *service_type;
788 control_point = GUPNP_CONTROL_POINT (resource_browser);
791 if (!parse_usn (usn, &udn, &service_type))
796 GList *l = find_service_node (control_point, udn, service_type);
799 GUPnPServiceProxy *proxy;
802 proxy = GUPNP_SERVICE_PROXY (l->data);
804 control_point->priv->services =
806 (control_point->priv->services, l);
808 g_signal_emit (control_point,
809 signals[SERVICE_PROXY_UNAVAILABLE],
813 g_object_unref (proxy);
816 GList *l = find_device_node (control_point, udn);
819 GUPnPDeviceProxy *proxy;
822 proxy = GUPNP_DEVICE_PROXY (l->data);
824 control_point->priv->devices =
826 (control_point->priv->devices, l);
828 g_signal_emit (control_point,
829 signals[DEVICE_PROXY_UNAVAILABLE],
833 g_object_unref (proxy);
838 g_free (service_type);
842 gupnp_control_point_set_property (GObject *object,
847 GUPnPControlPoint *control_point;
849 control_point = GUPNP_CONTROL_POINT (object);
851 switch (property_id) {
852 case PROP_RESOURCE_FACTORY:
853 control_point->priv->factory =
854 GUPNP_RESOURCE_FACTORY (g_value_dup_object (value));
857 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
863 gupnp_control_point_get_property (GObject *object,
868 GUPnPControlPoint *control_point;
870 control_point = GUPNP_CONTROL_POINT (object);
872 switch (property_id) {
873 case PROP_RESOURCE_FACTORY:
874 g_value_set_object (value,
875 gupnp_control_point_get_resource_factory (control_point));
878 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
884 gupnp_control_point_class_init (GUPnPControlPointClass *klass)
886 GObjectClass *object_class;
887 GSSDPResourceBrowserClass *browser_class;
889 object_class = G_OBJECT_CLASS (klass);
891 object_class->set_property = gupnp_control_point_set_property;
892 object_class->get_property = gupnp_control_point_get_property;
893 object_class->dispose = gupnp_control_point_dispose;
894 object_class->finalize = gupnp_control_point_finalize;
896 browser_class = GSSDP_RESOURCE_BROWSER_CLASS (klass);
898 browser_class->resource_available =
899 gupnp_control_point_resource_available;
900 browser_class->resource_unavailable =
901 gupnp_control_point_resource_unavailable;
903 g_type_class_add_private (klass, sizeof (GUPnPControlPointPrivate));
906 * GUPnPControlPoint:resource-factory:
908 * The resource factory to use. Set to NULL for default factory.
910 g_object_class_install_property
912 PROP_RESOURCE_FACTORY,
913 g_param_spec_object ("resource-factory",
915 "The resource factory to use",
916 GUPNP_TYPE_RESOURCE_FACTORY,
917 G_PARAM_CONSTRUCT_ONLY |
919 G_PARAM_STATIC_NAME |
920 G_PARAM_STATIC_NICK |
921 G_PARAM_STATIC_BLURB));
924 * GUPnPControlPoint::device-proxy-available:
925 * @control_point: The #GUPnPControlPoint that received the signal
926 * @proxy: The now available #GUPnPDeviceProxy
928 * The ::device-proxy-available signal is emitted whenever a new
929 * device has become available.
931 signals[DEVICE_PROXY_AVAILABLE] =
932 g_signal_new ("device-proxy-available",
933 GUPNP_TYPE_CONTROL_POINT,
935 G_STRUCT_OFFSET (GUPnPControlPointClass,
936 device_proxy_available),
939 g_cclosure_marshal_VOID__OBJECT,
942 GUPNP_TYPE_DEVICE_PROXY);
945 * GUPnPControlPoint::device-proxy-unavailable:
946 * @control_point: The #GUPnPControlPoint that received the signal
947 * @proxy: The now unavailable #GUPnPDeviceProxy
949 * The ::device-proxy-unavailable signal is emitted whenever a
950 * device is not available any more.
952 signals[DEVICE_PROXY_UNAVAILABLE] =
953 g_signal_new ("device-proxy-unavailable",
954 GUPNP_TYPE_CONTROL_POINT,
956 G_STRUCT_OFFSET (GUPnPControlPointClass,
957 device_proxy_unavailable),
960 g_cclosure_marshal_VOID__OBJECT,
963 GUPNP_TYPE_DEVICE_PROXY);
966 * GUPnPControlPoint::service-proxy-available:
967 * @control_point: The #GUPnPControlPoint that received the signal
968 * @proxy: The now available #GUPnPServiceProxy
970 * The ::service-proxy-available signal is emitted whenever a new
971 * service has become available.
973 signals[SERVICE_PROXY_AVAILABLE] =
974 g_signal_new ("service-proxy-available",
975 GUPNP_TYPE_CONTROL_POINT,
977 G_STRUCT_OFFSET (GUPnPControlPointClass,
978 service_proxy_available),
981 g_cclosure_marshal_VOID__OBJECT,
984 GUPNP_TYPE_SERVICE_PROXY);
987 * GUPnPControlPoint::service-proxy-unavailable:
988 * @control_point: The #GUPnPControlPoint that received the signal
989 * @proxy: The now unavailable #GUPnPServiceProxy
991 * The ::service-proxy-unavailable signal is emitted whenever a
992 * service is not available any more.
994 signals[SERVICE_PROXY_UNAVAILABLE] =
995 g_signal_new ("service-proxy-unavailable",
996 GUPNP_TYPE_CONTROL_POINT,
998 G_STRUCT_OFFSET (GUPnPControlPointClass,
999 service_proxy_unavailable),
1002 g_cclosure_marshal_VOID__OBJECT,
1005 GUPNP_TYPE_SERVICE_PROXY);
1009 * gupnp_control_point_new:
1010 * @context: A #GUPnPContext
1011 * @target: The search target
1013 * Create a new #GUPnPControlPoint with the specified @context and @target.
1015 * @target should be a service or device name, such as
1016 * <literal>urn:schemas-upnp-org:service:WANIPConnection:1</literal> or
1017 * <literal>urn:schemas-upnp-org:device:MediaRenderer:1</literal>.
1019 * Return value: A new #GUPnPControlPoint object.
1022 gupnp_control_point_new (GUPnPContext *context,
1025 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
1026 g_return_val_if_fail (target, NULL);
1028 return g_object_new (GUPNP_TYPE_CONTROL_POINT,
1035 * gupnp_control_point_new_full:
1036 * @context: A #GUPnPContext
1037 * @factory: A #GUPnPResourceFactory
1038 * @target: The search target
1040 * Create a new #GUPnPControlPoint with the specified @context, @factory and
1043 * @target should be a service or device name, such as
1044 * <literal>urn:schemas-upnp-org:service:WANIPConnection:1</literal> or
1045 * <literal>urn:schemas-upnp-org:device:MediaRenderer:1</literal>.
1047 * Return value: A new #GUPnPControlPoint object.
1050 gupnp_control_point_new_full (GUPnPContext *context,
1051 GUPnPResourceFactory *factory,
1054 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
1055 g_return_val_if_fail (factory == NULL ||
1056 GUPNP_IS_RESOURCE_FACTORY (factory), NULL);
1057 g_return_val_if_fail (target, NULL);
1059 return g_object_new (GUPNP_TYPE_CONTROL_POINT,
1062 "resource-factory", factory,
1067 * gupnp_control_point_get_context:
1068 * @control_point: A #GUPnPControlPoint
1070 * Get the #GUPnPControlPoint associated with @control_point.
1072 * Returns: (transfer none): The #GUPnPContext.
1075 gupnp_control_point_get_context (GUPnPControlPoint *control_point)
1077 GSSDPClient *client;
1079 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1081 client = gssdp_resource_browser_get_client
1082 (GSSDP_RESOURCE_BROWSER (control_point));
1084 return GUPNP_CONTEXT (client);
1088 * gupnp_control_point_list_device_proxies:
1089 * @control_point: A #GUPnPControlPoint
1091 * Get the #GList of discovered #GUPnPDeviceProxy objects. Do not free the list
1094 * Return value: (element-type GUPnP.DeviceProxy) (transfer none): a #GList of
1095 * #GUPnPDeviceProxy objects.
1098 gupnp_control_point_list_device_proxies (GUPnPControlPoint *control_point)
1100 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1102 return (const GList *) control_point->priv->devices;
1106 * gupnp_control_point_list_service_proxies:
1107 * @control_point: A #GUPnPControlPoint
1109 * Get the #GList of discovered #GUPnPServiceProxy objects. Do not free the
1110 * list nor its elements.
1112 * Return value: (element-type GUPnP.ServiceProxy) (transfer none): a #GList
1113 * of #GUPnPServiceProxy objects.
1116 gupnp_control_point_list_service_proxies (GUPnPControlPoint *control_point)
1118 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1120 return (const GList *) control_point->priv->services;
1124 * gupnp_control_point_get_resource_factory:
1125 * @control_point: A #GUPnPControlPoint
1127 * Get the #GUPnPResourceFactory used by the @control_point.
1129 * Returns: (transfer none): A #GUPnPResourceFactory.
1131 GUPnPResourceFactory *
1132 gupnp_control_point_get_resource_factory (GUPnPControlPoint *control_point)
1134 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1136 if (control_point->priv->factory)
1137 return control_point->priv->factory;
1139 return gupnp_resource_factory_get_default ();