Initial submission of GUPnP to Tizen IVI
[profile/ivi/GUPnP.git] / libgupnp / gupnp-service.c
1 /*
2  * Copyright (C) 2007, 2008 OpenedHand Ltd.
3  *
4  * Author: Jorn Baayen <jorn@openedhand.com>
5  *
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.
10  *
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.
15  *
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.
20  */
21
22 /**
23  * SECTION:gupnp-service
24  * @short_description: Class for service implementations.
25  *
26  * #GUPnPService allows for handling incoming actions and state variable
27  * notification. #GUPnPService implements the #GUPnPServiceInfo interface.
28  */
29
30 #include <gobject/gvaluecollector.h>
31 #include <gmodule.h>
32 #include <libsoup/soup-date.h>
33 #include <uuid/uuid.h>
34 #include <string.h>
35 #include "gupnp-service.h"
36 #include "gupnp-root-device.h"
37 #include "gupnp-context-private.h"
38 #include "gupnp-marshal.h"
39 #include "gupnp-error.h"
40 #include "http-headers.h"
41 #include "gena-protocol.h"
42 #include "xml-util.h"
43 #include "gvalue-util.h"
44
45 #define SUBSCRIPTION_TIMEOUT 300 /* DLNA (7.2.22.1) enforced */
46
47 G_DEFINE_TYPE (GUPnPService,
48                gupnp_service,
49                GUPNP_TYPE_SERVICE_INFO);
50
51 struct _GUPnPServicePrivate {
52         GUPnPRootDevice *root_device;
53
54         SoupSession     *session;
55
56         guint            notify_available_id;
57
58         GHashTable      *subscriptions;
59
60         GList           *state_variables;
61
62         GQueue          *notify_queue;
63
64         gboolean         notify_frozen;
65 };
66
67 enum {
68         PROP_0,
69         PROP_ROOT_DEVICE
70 };
71
72 enum {
73         ACTION_INVOKED,
74         QUERY_VARIABLE,
75         NOTIFY_FAILED,
76         LAST_SIGNAL
77 };
78
79 static guint signals[LAST_SIGNAL];
80
81 static char *
82 create_property_set (GQueue *queue);
83
84 static void
85 notify_subscriber   (gpointer key,
86                      gpointer value,
87                      gpointer user_data);
88
89 typedef struct {
90         GUPnPService *service;
91
92         GList        *callbacks;
93         char         *sid;
94
95         int           seq;
96
97         GSource      *timeout_src;
98
99         GList        *pending_messages; /* Pending SoupMessages from this
100                                            subscription */
101         gboolean      initial_state_sent;
102         gboolean      to_delete;
103 } SubscriptionData;
104
105 static gboolean
106 subscription_data_can_delete (SubscriptionData *data) {
107     return data->initial_state_sent && data->to_delete;
108 }
109
110 static void
111 send_initial_state (SubscriptionData *data);
112
113 static SoupSession *
114 gupnp_service_get_session (GUPnPService *service)
115 {
116         if (! service->priv->session) {
117                 /* Create a dedicated session for this service to
118                  * ensure that notifications are sent in the proper
119                  * order. The session from GUPnPContext may use
120                  * multiple connections.
121                  */
122                 service->priv->session = soup_session_async_new_with_options
123                   (SOUP_SESSION_IDLE_TIMEOUT, 60,
124                    SOUP_SESSION_ASYNC_CONTEXT,
125                    g_main_context_get_thread_default (),
126                    SOUP_SESSION_MAX_CONNS_PER_HOST, 1,
127                    NULL);
128
129                 if (g_getenv ("GUPNP_DEBUG")) {
130                         SoupLogger *logger;
131                         logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
132                         soup_session_add_feature (
133                                         service->priv->session,
134                                         SOUP_SESSION_FEATURE (logger));
135                 }
136         }
137
138         return service->priv->session;
139 }
140
141 static void
142 subscription_data_free (SubscriptionData *data)
143 {
144         SoupSession *session;
145
146         session = gupnp_service_get_session (data->service);
147
148         /* Cancel pending messages */
149         while (data->pending_messages) {
150                 SoupMessage *msg;
151
152                 msg = data->pending_messages->data;
153
154                 soup_session_cancel_message (session,
155                                              msg,
156                                              SOUP_STATUS_CANCELLED);
157
158                 data->pending_messages =
159                         g_list_delete_link (data->pending_messages,
160                                             data->pending_messages);
161         }
162        
163         /* Further cleanup */
164         while (data->callbacks) {
165                 g_free (data->callbacks->data);
166                 data->callbacks = g_list_delete_link (data->callbacks,
167                                                       data->callbacks);
168         }
169
170         g_free (data->sid);
171
172         if (data->timeout_src)
173                 g_source_destroy (data->timeout_src);
174
175         g_slice_free (SubscriptionData, data);
176 }
177
178 typedef struct {
179         char  *variable;
180         GValue value;
181 } NotifyData;
182
183 static void
184 notify_data_free (NotifyData *data)
185 {
186         g_free (data->variable);
187         g_value_unset (&data->value);
188
189         g_slice_free (NotifyData, data);
190 }
191
192 struct _GUPnPServiceAction {
193         volatile gint ref_count;
194
195         GUPnPContext *context;
196
197         char         *name;
198
199         SoupMessage  *msg;
200         gboolean      accept_gzip;
201
202         GUPnPXMLDoc  *doc;
203         xmlNode      *node;
204
205         GString      *response_str;
206
207         guint         argument_count;
208 };
209
210 GUPnPServiceAction *
211 gupnp_service_action_ref (GUPnPServiceAction *action)
212 {
213         g_return_val_if_fail (action, NULL);
214         g_return_val_if_fail (action->ref_count > 0, NULL);
215
216         g_atomic_int_inc (&action->ref_count);
217
218         return action;
219 }
220
221 void
222 gupnp_service_action_unref (GUPnPServiceAction *action)
223 {
224         g_return_if_fail (action);
225         g_return_if_fail (action->ref_count > 0);
226
227         if (g_atomic_int_dec_and_test (&action->ref_count)) {
228                 g_free (action->name);
229                 g_object_unref (action->msg);
230                 g_object_unref (action->context);
231                 g_object_unref (action->doc);
232
233                 g_slice_free (GUPnPServiceAction, action);
234         }
235 }
236
237 /**
238  * gupnp_service_action_get_type:
239  *
240  * Get the gtype for GUPnPServiceActon
241  *
242  * Return value: The gtype of GUPnPServiceAction
243  **/
244 GType
245 gupnp_service_action_get_type (void)
246 {
247         static GType our_type = 0;
248
249         if (our_type == 0)
250                 our_type = g_boxed_type_register_static
251                         ("GUPnPServiceAction",
252                          (GBoxedCopyFunc) gupnp_service_action_ref,
253                          (GBoxedFreeFunc) gupnp_service_action_unref);
254
255         return our_type;
256 }
257
258 static void
259 finalize_action (GUPnPServiceAction *action)
260 {
261         SoupServer *server;
262
263         /* Embed action->response_str in a SOAP document */
264         g_string_prepend (action->response_str,
265                           "<?xml version=\"1.0\"?>"
266                           "<s:Envelope xmlns:s="
267                                 "\"http://schemas.xmlsoap.org/soap/envelope/\" "
268                           "s:encodingStyle="
269                                 "\"http://schemas.xmlsoap.org/soap/encoding/\">"
270                           "<s:Body>");
271
272         if (action->msg->status_code != SOUP_STATUS_INTERNAL_SERVER_ERROR) {
273                 g_string_append (action->response_str, "</u:");
274                 g_string_append (action->response_str, action->name);
275                 g_string_append (action->response_str, "Response>");
276         }
277
278         g_string_append (action->response_str,
279                          "</s:Body>"
280                          "</s:Envelope>");
281
282         soup_message_headers_replace (action->msg->response_headers,
283                                       "Content-Type",
284                                       "text/xml; charset=\"utf-8\"");
285
286         if (action->accept_gzip && action->response_str->len > 1024) {
287                 http_response_set_body_gzip (action->msg,
288                                              action->response_str->str,
289                                              action->response_str->len);
290                 g_string_free (action->response_str, TRUE);
291         } else {
292                 soup_message_body_append (action->msg->response_body,
293                                           SOUP_MEMORY_TAKE,
294                                           action->response_str->str,
295                                           action->response_str->len);
296                 g_string_free (action->response_str, FALSE);
297         }
298
299         soup_message_headers_append (action->msg->response_headers, "Ext", "");
300
301         /* Server header on response */
302         soup_message_headers_append
303                         (action->msg->response_headers,
304                          "Server",
305                          gssdp_client_get_server_id
306                                 (GSSDP_CLIENT (action->context)));
307
308         /* Tell soup server that response is now ready */
309         server = gupnp_context_get_server (action->context);
310         soup_server_unpause_message (server, action->msg);
311
312         /* Cleanup */
313         gupnp_service_action_unref (action);
314 }
315
316 /**
317  * gupnp_service_action_get_name:
318  * @action: A #GUPnPServiceAction
319  *
320  * Get the name of @action.
321  *
322  * Return value: The name of @action
323  **/
324 const char *
325 gupnp_service_action_get_name (GUPnPServiceAction *action)
326 {
327         g_return_val_if_fail (action != NULL, NULL);
328
329         return action->name;
330 }
331
332 /**
333  * gupnp_service_action_get_locales:
334  * @action: A #GUPnPServiceAction
335  *
336  * Get an ordered (preferred first) #GList of locales preferred by
337  * the client. Free list and elements after use.
338  *
339  * Return value: (element-type utf8) (transfer full): A #GList of #char*
340  * locale names.
341  **/
342 GList *
343 gupnp_service_action_get_locales (GUPnPServiceAction *action)
344 {
345         g_return_val_if_fail (action != NULL, NULL);
346
347         return http_request_get_accept_locales (action->msg);
348 }
349
350 /**
351  * gupnp_service_action_get:
352  * @action: A #GUPnPServiceAction
353  * @Varargs: tuples of argument name, argument type, and argument value
354  * location, terminated with %NULL.
355  *
356  * Retrieves the specified action arguments.
357  **/
358 void
359 gupnp_service_action_get (GUPnPServiceAction *action,
360                           ...)
361 {
362         va_list var_args;
363
364         g_return_if_fail (action != NULL);
365
366         va_start (var_args, action);
367         gupnp_service_action_get_valist (action, var_args);
368         va_end (var_args);
369 }
370
371 /**
372  * gupnp_service_action_get_valist:
373  * @action: A #GUPnPServiceAction
374  * @var_args: va_list of tuples of argument name, argument type, and argument
375  * value location.
376  *
377  * See gupnp_service_action_get(); this version takes a va_list for
378  * use by language bindings.
379  **/
380 void
381 gupnp_service_action_get_valist (GUPnPServiceAction *action,
382                                  va_list             var_args)
383 {
384         const char *arg_name;
385         GType arg_type;
386         GValue value = {0, };
387         char *copy_error;
388
389         g_return_if_fail (action != NULL);
390
391         copy_error = NULL;
392
393         arg_name = va_arg (var_args, const char *);
394         while (arg_name) {
395                 arg_type = va_arg (var_args, GType);
396                 g_value_init (&value, arg_type);
397
398                 gupnp_service_action_get_value (action, arg_name, &value);
399
400                 G_VALUE_LCOPY (&value, var_args, 0, &copy_error);
401
402                 g_value_unset (&value);
403
404                 if (copy_error) {
405                         g_warning ("Error lcopying value: %s\n", copy_error);
406
407                         g_free (copy_error);
408                 }
409
410                 arg_name = va_arg (var_args, const char *);
411         }
412 }
413
414 /**
415  * gupnp_service_action_get_values:
416  * @action: A #GUPnPServiceAction
417  * @arg_names: (element-type utf8) : A #GList of argument names as string
418  * @arg_types: (element-type GType): A #GList of argument types as #GType
419  *
420  * A variant of #gupnp_service_action_get that uses #GList instead of varargs.
421  *
422  * Return value: (element-type GValue) (transfer full): The values as #GList of
423  * #GValue. #g_list_free the returned list and #g_value_unset and #g_slice_free
424  * each element.
425  **/
426 GList *
427 gupnp_service_action_get_values (GUPnPServiceAction *action,
428                                  GList              *arg_names,
429                                  GList              *arg_types)
430 {
431         GList *arg_values;
432         int i;
433
434         g_return_val_if_fail (action != NULL, NULL);
435
436         arg_values = NULL;
437
438         for (i = 0; i < g_list_length (arg_names); i++) {
439                 const char *arg_name;
440                 GType arg_type;
441                 GValue *arg_value;
442
443                 arg_name = (const char *) g_list_nth_data (arg_names, i);
444                 arg_type = (GType) g_list_nth_data (arg_types, i);
445
446                 arg_value = g_slice_new0 (GValue);
447                 g_value_init (arg_value, arg_type);
448
449                 gupnp_service_action_get_value (action, arg_name, arg_value);
450
451                 arg_values = g_list_append (arg_values, arg_value);
452         }
453
454         return arg_values;
455 }
456
457 /**
458  * gupnp_service_action_get_value: (skip)
459  * @action: A #GUPnPServiceAction
460  * @argument: The name of the argument to retrieve
461  * @value: The #GValue to store the value of the argument, initialized
462  * to the correct type.
463  *
464  * Retrieves the value of @argument into @value.
465  **/
466 void
467 gupnp_service_action_get_value (GUPnPServiceAction *action,
468                                 const char         *argument,
469                                 GValue             *value)
470 {
471         xmlNode *node;
472         gboolean found;
473
474         g_return_if_fail (action != NULL);
475         g_return_if_fail (argument != NULL);
476         g_return_if_fail (value != NULL);
477
478         found = FALSE;
479         for (node = action->node->children; node; node = node->next) {
480                 if (strcmp ((char *) node->name, argument) != 0)
481                         continue;
482
483                 found = gvalue_util_set_value_from_xml_node (value, node);
484
485                 break;
486         }
487
488         if (!found)
489                 g_warning ("Failed to retreive '%s' argument of '%s' action",
490                            argument,
491                            action->name);
492 }
493
494 /**
495  * gupnp_service_action_get_gvalue:
496  * @action: A #GUPnPServiceAction
497  * @argument: The name of the argument to retrieve
498  * @type: The type of argument to retrieve
499  *
500  * Retrieves the value of @argument into a GValue of type @type and returns it.
501  * The method exists only and only to satify PyGI, please use
502  * #gupnp_service_action_get_value and ignore this if possible.
503  *
504  * Return value: (transfer full): Value as #GValue associated with @action.
505  * #g_value_unset and #g_slice_free it after usage.
506  *
507  * Rename To: gupnp_service_action_get_value
508  **/
509 GValue *
510 gupnp_service_action_get_gvalue (GUPnPServiceAction *action,
511                                        const char         *argument,
512                                        GType               type)
513 {
514         GValue *val;
515
516         val = g_slice_new0 (GValue);
517         g_value_init (val, type);
518
519         gupnp_service_action_get_value (action, argument, val);
520
521         return val;
522 }
523
524 /**
525  * gupnp_service_action_get_argument_count:
526  * @action: A #GUPnPServiceAction
527  *
528  * Get the number of IN arguments from the @action and return it.
529  *
530  * Return value: The number of IN arguments from the @action.
531  */
532 guint
533 gupnp_service_action_get_argument_count (GUPnPServiceAction *action)
534 {
535     return action->argument_count;
536 }
537
538 /**
539  * gupnp_service_action_set:
540  * @action: A #GUPnPServiceAction
541  * @Varargs: tuples of return value name, return value type, and
542  * actual return value, terminated with %NULL.
543  *
544  * Sets the specified action return values.
545  **/
546 void
547 gupnp_service_action_set (GUPnPServiceAction *action,
548                           ...)
549 {
550         va_list var_args;
551
552         g_return_if_fail (action != NULL);
553
554         va_start (var_args, action);
555         gupnp_service_action_set_valist (action, var_args);
556         va_end (var_args);
557 }
558
559 /**
560  * gupnp_service_action_set_valist:
561  * @action: A #GUPnPServiceAction
562  * @var_args: va_list of tuples of return value name, return value type, and
563  * actual return value.
564  *
565  * See gupnp_service_action_set(); this version takes a va_list for
566  * use by language bindings.
567  **/
568 void
569 gupnp_service_action_set_valist (GUPnPServiceAction *action,
570                                  va_list             var_args)
571 {
572         const char *arg_name;
573         GType arg_type;
574         GValue value = {0, };
575         char *collect_error;
576
577         g_return_if_fail (action != NULL);
578
579         collect_error = NULL;
580
581         arg_name = va_arg (var_args, const char *);
582         while (arg_name) {
583                 arg_type = va_arg (var_args, GType);
584                 g_value_init (&value, arg_type);
585
586                 G_VALUE_COLLECT (&value, var_args, G_VALUE_NOCOPY_CONTENTS,
587                                  &collect_error);
588                 if (!collect_error) {
589                         gupnp_service_action_set_value (action,
590                                                         arg_name, &value);
591
592                         g_value_unset (&value);
593
594                 } else {
595                         g_warning ("Error collecting value: %s\n",
596                                    collect_error);
597
598                         g_free (collect_error);
599                 }
600
601                 arg_name = va_arg (var_args, const char *);
602         }
603 }
604
605 /**
606  * gupnp_service_action_set_values:
607  * @action: A #GUPnPServiceAction
608  * @arg_names: (element-type utf8) (transfer none): A #GList of argument names
609  * @arg_values: (element-type GValue) (transfer none): The #GList of values (as
610  * #GValues) that line up with @arg_names.
611  *
612  * Sets the specified action return values.
613  **/
614 void
615 gupnp_service_action_set_values (GUPnPServiceAction *action,
616                                  GList              *arg_names,
617                                  GList              *arg_values)
618 {
619         g_return_if_fail (action != NULL);
620         g_return_if_fail (arg_names != NULL);
621         g_return_if_fail (arg_values != NULL);
622         g_return_if_fail (g_list_length (arg_names) ==
623                           g_list_length (arg_values));
624
625         if (action->msg->status_code == SOUP_STATUS_INTERNAL_SERVER_ERROR) {
626                 g_warning ("Calling gupnp_service_action_set_value() after "
627                            "having called gupnp_service_action_return_error() "
628                            "is not allowed.");
629
630                 return;
631         }
632
633         /* Append to response */
634         for (; arg_names; arg_names = arg_names->next) {
635                 const char *arg_name;
636                 GValue *value;
637
638                 arg_name = arg_names->data;
639                 value = arg_values->data;
640
641                 xml_util_start_element (action->response_str, arg_name);
642                 gvalue_util_value_append_to_xml_string (value,
643                                                         action->response_str);
644                 xml_util_end_element (action->response_str, arg_name);
645
646                 arg_values = arg_values->next;
647         }
648 }
649
650 /**
651  * gupnp_service_action_set_value:
652  * @action: A #GUPnPServiceAction
653  * @argument: The name of the return value to retrieve
654  * @value: The #GValue to store the return value
655  *
656  * Sets the value of @argument to @value.
657  **/
658 void
659 gupnp_service_action_set_value (GUPnPServiceAction *action,
660                                 const char         *argument,
661                                 const GValue       *value)
662 {
663         g_return_if_fail (action != NULL);
664         g_return_if_fail (argument != NULL);
665         g_return_if_fail (value != NULL);
666
667         if (action->msg->status_code == SOUP_STATUS_INTERNAL_SERVER_ERROR) {
668                 g_warning ("Calling gupnp_service_action_set_value() after "
669                            "having called gupnp_service_action_return_error() "
670                            "is not allowed.");
671
672                 return;
673         }
674
675         /* Append to response */
676         xml_util_start_element (action->response_str, argument);
677         gvalue_util_value_append_to_xml_string (value, action->response_str);
678         xml_util_end_element (action->response_str, argument);
679 }
680
681 /**
682  * gupnp_service_action_return:
683  * @action: A #GUPnPServiceAction
684  *
685  * Return succesfully.
686  **/
687 void
688 gupnp_service_action_return (GUPnPServiceAction *action)
689 {
690         g_return_if_fail (action != NULL);
691
692         soup_message_set_status (action->msg, SOUP_STATUS_OK);
693
694         finalize_action (action);
695 }
696
697 /**
698  * gupnp_service_action_return_error:
699  * @action: A #GUPnPServiceAction
700  * @error_code: The error code
701  * @error_description: The error description, or %NULL if @error_code is
702  * one of #GUPNP_CONTROL_ERROR_INVALID_ACTION,
703  * #GUPNP_CONTROL_ERROR_INVALID_ARGS, #GUPNP_CONTROL_ERROR_OUT_OF_SYNC or
704  * #GUPNP_CONTROL_ERROR_ACTION_FAILED, in which case a description is
705  * provided automatically.
706  *
707  * Return @error_code.
708  **/
709 void
710 gupnp_service_action_return_error (GUPnPServiceAction *action,
711                                    guint               error_code,
712                                    const char         *error_description)
713 {
714         g_return_if_fail (action != NULL);
715
716         switch (error_code) {
717         case GUPNP_CONTROL_ERROR_INVALID_ACTION:
718                 if (error_description == NULL)
719                         error_description = "Invalid Action";
720
721                 break;
722         case GUPNP_CONTROL_ERROR_INVALID_ARGS:
723                 if (error_description == NULL)
724                         error_description = "Invalid Args";
725
726                 break;
727         case GUPNP_CONTROL_ERROR_OUT_OF_SYNC:
728                 if (error_description == NULL)
729                         error_description = "Out of Sync";
730
731                 break;
732         case GUPNP_CONTROL_ERROR_ACTION_FAILED:
733                 if (error_description == NULL)
734                         error_description = "Action Failed";
735
736                 break;
737         default:
738                 g_return_if_fail (error_description != NULL);
739                 break;
740         }
741
742         /* Replace response_str with a SOAP Fault */
743         g_string_erase (action->response_str, 0, -1);
744
745         xml_util_start_element (action->response_str, "s:Fault");
746
747         xml_util_start_element (action->response_str, "faultcode");
748         g_string_append (action->response_str, "s:Client");
749         xml_util_end_element (action->response_str, "faultcode");
750
751         xml_util_start_element (action->response_str, "faultstring");
752         g_string_append (action->response_str, "UPnPError");
753         xml_util_end_element (action->response_str, "faultstring");
754
755         xml_util_start_element (action->response_str, "detail");
756
757         xml_util_start_element (action->response_str,
758                                 "UPnPError "
759                                 "xmlns=\"urn:schemas-upnp-org:control-1-0\"");
760
761         xml_util_start_element (action->response_str, "errorCode");
762         g_string_append_printf (action->response_str, "%u", error_code);
763         xml_util_end_element (action->response_str, "errorCode");
764
765         xml_util_start_element (action->response_str, "errorDescription");
766         xml_util_add_content (action->response_str, error_description);
767         xml_util_end_element (action->response_str, "errorDescription");
768
769         xml_util_end_element (action->response_str, "UPnPError");
770         xml_util_end_element (action->response_str, "detail");
771
772         xml_util_end_element (action->response_str, "s:Fault");
773
774         soup_message_set_status (action->msg,
775                                  SOUP_STATUS_INTERNAL_SERVER_ERROR);
776
777         finalize_action (action);
778 }
779
780 /**
781  * gupnp_service_action_get_message:
782  * @action: A #GUPnPServiceAction
783  *
784  * Get the #SoupMessage associated with @action. Mainly intended for
785  * applications to be able to read HTTP headers received from clients.
786  *
787  * Return value: (transfer full): #SoupMessage associated with @action. Unref
788  * after using it.
789  **/
790 SoupMessage *
791 gupnp_service_action_get_message (GUPnPServiceAction *action)
792 {
793         return g_object_ref (action->msg);
794 }
795
796 static void
797 gupnp_service_init (GUPnPService *service)
798 {
799         service->priv = G_TYPE_INSTANCE_GET_PRIVATE (service,
800                                                      GUPNP_TYPE_SERVICE,
801                                                      GUPnPServicePrivate);
802
803         service->priv->subscriptions =
804                 g_hash_table_new_full (g_str_hash,
805                                        g_str_equal,
806                                        NULL,
807                                        (GDestroyNotify) subscription_data_free);
808
809         service->priv->notify_queue = g_queue_new ();
810 }
811
812 /* Generate a new action response node for @action_name */
813 static GString *
814 new_action_response_str (const char   *action_name,
815                          const char   *service_type)
816 {
817         GString *str;
818
819         str = xml_util_new_string ();
820
821         g_string_append (str, "<u:");
822         g_string_append (str, action_name);
823         g_string_append (str, "Response xmlns:u=");
824
825         if (service_type != NULL) {
826                 g_string_append (str, service_type);
827                 g_string_append_c (str, '"');
828         } else {
829                 g_warning ("No serviceType defined. Control may not work "
830                            "correctly.");
831         }
832
833         g_string_append_c (str, '>');
834
835         return str;
836 }
837
838 /* Handle QueryStateVariable action */
839 static void
840 query_state_variable (GUPnPService       *service,
841                       GUPnPServiceAction *action)
842 {
843         xmlNode *node;
844
845         /* Iterate requested variables */
846         for (node = action->node->children; node; node = node->next) {
847                 xmlChar *var_name;
848                 GValue value = {0,};
849
850                 if (strcmp ((char *) node->name, "varName") != 0)
851                         continue;
852
853                 /* varName */
854                 var_name = xmlNodeGetContent (node);
855                 if (!var_name) {
856                         gupnp_service_action_return_error (action,
857                                                            402,
858                                                            "Invalid Args");
859
860                         return;
861                 }
862
863                 /* Query variable */
864                 g_signal_emit (service,
865                                signals[QUERY_VARIABLE],
866                                g_quark_from_string ((char *) var_name),
867                                (char *) var_name,
868                                &value);
869
870                 if (!G_IS_VALUE (&value)) {
871                         gupnp_service_action_return_error (action,
872                                                            402,
873                                                            "Invalid Args");
874
875                         xmlFree (var_name);
876
877                         return;
878                 }
879
880                 /* Add variable to response */
881                 gupnp_service_action_set_value (action,
882                                                 (char *) var_name,
883                                                 &value);
884
885                 /* Cleanup */
886                 g_value_unset (&value);
887
888                 xmlFree (var_name);
889         }
890
891         gupnp_service_action_return (action);
892 }
893
894 /* controlURL handler */
895 static void
896 control_server_handler (SoupServer        *server,
897                         SoupMessage       *msg, 
898                         const char        *server_path,
899                         GHashTable        *query,
900                         SoupClientContext *soup_client,
901                         gpointer           user_data)
902 {
903         GUPnPService *service;
904         GUPnPContext *context;
905         xmlDoc *doc;
906         xmlNode *action_node, *node;
907         const char *soap_action;
908         const char *accept_encoding;
909         char *action_name;
910         char *end;
911         GUPnPServiceAction *action;
912
913         service = GUPNP_SERVICE (user_data);
914
915         if (msg->method != SOUP_METHOD_POST) {
916                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
917
918                 return;
919         }
920
921         if (msg->request_body->length == 0) {
922                 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
923
924                 return;
925         }
926
927         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (service));
928
929         /* Get action name */
930         soap_action = soup_message_headers_get_one (msg->request_headers,
931                                                     "SOAPAction");
932         if (!soap_action) {
933                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
934                 return;
935         }
936
937         action_name = strchr (soap_action, '#');
938         if (!action_name) {
939                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
940
941                 return;
942         }
943
944         /* This memory is libsoup-owned so we can do this */
945         *action_name = '\0';
946         action_name += 1;
947
948         /* This memory is libsoup-owned so we can do this */
949         end = strrchr (action_name, '"');
950         if (end)
951                 *end = '\0';
952
953         /* Parse action_node */
954         doc = xmlRecoverMemory (msg->request_body->data,
955                                 msg->request_body->length);
956         if (doc == NULL) {
957                 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
958
959                 return;
960         }
961
962         action_node = xml_util_get_element ((xmlNode *) doc,
963                                             "Envelope",
964                                             "Body",
965                                             action_name,
966                                             NULL);
967         if (!action_node) {
968                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
969
970                 return;
971         }
972
973         /* Create action structure */
974         action = g_slice_new0 (GUPnPServiceAction);
975
976         action->ref_count      = 1;
977         action->name           = g_strdup (action_name);
978         action->msg            = g_object_ref (msg);
979         action->doc            = gupnp_xml_doc_new(doc);
980         action->node           = action_node;
981         action->response_str   = new_action_response_str (action_name,
982                                                         soap_action);
983         action->context        = g_object_ref (context);
984         action->argument_count = 0;
985
986         for (node = action->node->children; node; node = node->next)
987                 if (node->type == XML_ELEMENT_NODE)
988                         action->argument_count++;
989
990         /* Get accepted encodings */
991         accept_encoding = soup_message_headers_get_list (msg->request_headers,
992                                                          "Accept-Encoding");
993
994         if (accept_encoding) {
995                 GSList *codings;
996
997                 codings = soup_header_parse_quality_list (accept_encoding,
998                                                           NULL);
999                 if (codings &&
1000                     g_slist_find_custom (codings, "gzip",
1001                                          (GCompareFunc) g_ascii_strcasecmp)) {
1002                        action->accept_gzip = TRUE;
1003                 }
1004
1005                 soup_header_free_list (codings);
1006         }
1007
1008         /* Tell soup server that response is not ready yet */
1009         soup_server_pause_message (server, msg);
1010
1011         /* QueryStateVariable? */
1012         if (strcmp (action_name, "QueryStateVariable") == 0)
1013                 query_state_variable (service, action);
1014         else {
1015                 GQuark action_name_quark;
1016
1017                 action_name_quark = g_quark_from_string (action_name);
1018                 if (g_signal_has_handler_pending (service,
1019                                                   signals[ACTION_INVOKED],
1020                                                   action_name_quark,
1021                                                   FALSE)) {
1022                         /* Emit signal. Handler parses request and fills in
1023                          * response. */
1024                         g_signal_emit (service,
1025                                        signals[ACTION_INVOKED],
1026                                        action_name_quark,
1027                                        action);
1028                 } else {
1029                         /* No handlers attached. */
1030                         gupnp_service_action_return_error (action,
1031                                                            401,
1032                                                            "Invalid Action");
1033                 }
1034         }
1035 }
1036
1037 /* Generates a standard (re)subscription response */
1038 static void
1039 subscription_response (GUPnPService *service,
1040                        SoupMessage  *msg,
1041                        const char   *sid,
1042                        int           timeout)
1043 {
1044         GUPnPContext *context;
1045         GSSDPClient *client;
1046         char *tmp;
1047
1048         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (service));
1049         client = GSSDP_CLIENT (context);
1050
1051         /* Server header on response */
1052         soup_message_headers_append (msg->response_headers,
1053                                      "Server",
1054                                      gssdp_client_get_server_id (client));
1055
1056         /* SID header */
1057         soup_message_headers_append (msg->response_headers,
1058                                      "SID",
1059                                      sid);
1060
1061         /* Timeout header */
1062         if (timeout > 0)
1063                 tmp = g_strdup_printf ("Second-%d", timeout);
1064         else
1065                 tmp = g_strdup ("infinite");
1066
1067         soup_message_headers_append (msg->response_headers,
1068                                      "Timeout",
1069                                      tmp);
1070         g_free (tmp);
1071
1072         /* 200 OK */
1073         soup_message_set_status (msg, SOUP_STATUS_OK);
1074 }
1075
1076 /* Generates a new SID */
1077 static char *
1078 generate_sid (void)
1079 {
1080         uuid_t id;
1081         char out[39];
1082
1083         uuid_generate (id);
1084         uuid_unparse (id, out);
1085
1086         return g_strdup_printf ("uuid:%s", out);
1087 }
1088
1089 /* Subscription expired */
1090 static gboolean
1091 subscription_timeout (gpointer user_data)
1092 {
1093         SubscriptionData *data;
1094
1095         data = user_data;
1096
1097         g_hash_table_remove (data->service->priv->subscriptions, data->sid);
1098
1099         return FALSE;
1100 }
1101
1102 static void
1103 send_initial_state (SubscriptionData *data)
1104 {
1105         GQueue *queue;
1106         char *mem;
1107         GList *l;
1108
1109         /* Send initial event message */
1110         queue = g_queue_new ();
1111
1112         for (l = data->service->priv->state_variables; l; l = l->next) {
1113                 NotifyData *ndata;
1114
1115                 ndata = g_slice_new0 (NotifyData);
1116
1117                 g_signal_emit (data->service,
1118                                signals[QUERY_VARIABLE],
1119                                g_quark_from_string (l->data),
1120                                l->data,
1121                                &ndata->value);
1122
1123                 if (!G_IS_VALUE (&ndata->value)) {
1124                         g_slice_free (NotifyData, ndata);
1125
1126                         continue;
1127                 }
1128
1129                 ndata->variable = g_strdup (l->data);
1130
1131                 g_queue_push_tail (queue, ndata);
1132         }
1133
1134         mem = create_property_set (queue);
1135         notify_subscriber (data->sid, data, mem);
1136
1137         /* Cleanup */
1138         g_queue_free (queue);
1139
1140         g_free (mem);
1141 }
1142
1143
1144 /* Subscription request */
1145 static void
1146 subscribe (GUPnPService *service,
1147            SoupMessage  *msg,
1148            const char   *callback)
1149 {
1150         SubscriptionData *data;
1151         char *start, *end, *uri;
1152
1153         data = g_slice_new0 (SubscriptionData);
1154
1155         /* Parse callback list */
1156         start = (char *) callback;
1157         while ((start = strchr (start, '<'))) {
1158                 start += 1;
1159                 if (!start || !*start)
1160                         break;
1161
1162                 end = strchr (start, '>');
1163                 if (!end || !*end)
1164                         break;
1165
1166                 if (strncmp (start, "http://", strlen ("http://")) == 0) {
1167                         uri = g_strndup (start, end - start);
1168                         data->callbacks = g_list_append (data->callbacks, uri);
1169                 }
1170
1171                 start = end;
1172         }
1173
1174         if (!data->callbacks) {
1175                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1176
1177                 g_slice_free (SubscriptionData, data);
1178
1179                 return;
1180         }
1181
1182         /* Add service and SID */
1183         data->service = service;
1184         data->sid     = generate_sid ();
1185
1186         /* Add timeout */
1187         data->timeout_src = g_timeout_source_new_seconds (SUBSCRIPTION_TIMEOUT);
1188         g_source_set_callback (data->timeout_src,
1189                                subscription_timeout,
1190                                data,
1191                                NULL);
1192
1193         g_source_attach (data->timeout_src,
1194                          g_main_context_get_thread_default ());
1195
1196         g_source_unref (data->timeout_src);
1197
1198         /* Add to hash */
1199         g_hash_table_insert (service->priv->subscriptions,
1200                              data->sid,
1201                              data);
1202
1203         /* Respond */
1204         subscription_response (service, msg, data->sid, SUBSCRIPTION_TIMEOUT);
1205
1206         send_initial_state (data);
1207 }
1208
1209 /* Resubscription request */
1210 static void
1211 resubscribe (GUPnPService *service,
1212              SoupMessage  *msg,
1213              const char   *sid)
1214 {
1215         SubscriptionData *data;
1216
1217         data = g_hash_table_lookup (service->priv->subscriptions, sid);
1218         if (!data) {
1219                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1220
1221                 return;
1222         }
1223
1224         /* Update timeout */
1225         if (data->timeout_src) {
1226                 g_source_destroy (data->timeout_src);
1227                 data->timeout_src = NULL;
1228         }
1229
1230         data->timeout_src = g_timeout_source_new_seconds (SUBSCRIPTION_TIMEOUT);
1231         g_source_set_callback (data->timeout_src,
1232                                subscription_timeout,
1233                                data,
1234                                NULL);
1235
1236         g_source_attach (data->timeout_src,
1237                          g_main_context_get_thread_default ());
1238
1239         g_source_unref (data->timeout_src);
1240
1241         /* Respond */
1242         subscription_response (service, msg, sid, SUBSCRIPTION_TIMEOUT);
1243 }
1244
1245 /* Unsubscription request */
1246 static void
1247 unsubscribe (GUPnPService *service,
1248              SoupMessage  *msg,
1249              const char   *sid)
1250 {
1251         SubscriptionData *data;
1252
1253         data = g_hash_table_lookup (service->priv->subscriptions, sid);
1254         if (data) {
1255                 if (data->initial_state_sent)
1256                         g_hash_table_remove (service->priv->subscriptions,
1257                                              sid);
1258                 else
1259                         data->to_delete = TRUE;
1260                 soup_message_set_status (msg, SOUP_STATUS_OK);
1261         } else
1262                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1263 }
1264
1265 /* eventSubscriptionURL handler */
1266 static void
1267 subscription_server_handler (SoupServer        *server,
1268                              SoupMessage       *msg, 
1269                              const char        *server_path,
1270                              GHashTable        *query,
1271                              SoupClientContext *soup_client,
1272                              gpointer           user_data)
1273 {
1274         GUPnPService *service;
1275         const char *callback, *nt, *sid;
1276
1277         service = GUPNP_SERVICE (user_data);
1278
1279         callback = soup_message_headers_get_one (msg->request_headers,
1280                                                  "Callback");
1281         nt       = soup_message_headers_get_one (msg->request_headers, "NT");
1282         sid      = soup_message_headers_get_one (msg->request_headers, "SID");
1283
1284         /* Choose appropriate handler */
1285         if (strcmp (msg->method, GENA_METHOD_SUBSCRIBE) == 0) {
1286                 if (callback) {
1287                         if (sid) {
1288                                 soup_message_set_status
1289                                         (msg, SOUP_STATUS_BAD_REQUEST);
1290
1291                         } else if (!nt || strcmp (nt, "upnp:event") != 0) {
1292                                 soup_message_set_status
1293                                         (msg, SOUP_STATUS_PRECONDITION_FAILED);
1294
1295                         } else {
1296                                 subscribe (service, msg, callback);
1297
1298                         }
1299
1300                 } else if (sid) {
1301                         if (nt) {
1302                                 soup_message_set_status
1303                                         (msg, SOUP_STATUS_BAD_REQUEST);
1304
1305                         } else {
1306                                 resubscribe (service, msg, sid);
1307
1308                         }
1309
1310                 } else {
1311                         soup_message_set_status
1312                                 (msg, SOUP_STATUS_PRECONDITION_FAILED);
1313
1314                 }
1315
1316         } else if (strcmp (msg->method, GENA_METHOD_UNSUBSCRIBE) == 0) {
1317                 if (sid) {
1318                         if (nt || callback) {
1319                                 soup_message_set_status
1320                                         (msg, SOUP_STATUS_BAD_REQUEST);
1321
1322                         } else {
1323                                 unsubscribe (service, msg, sid);
1324
1325                         }
1326
1327                 } else {
1328                         soup_message_set_status
1329                                 (msg, SOUP_STATUS_PRECONDITION_FAILED);
1330
1331                 }
1332
1333         } else {
1334                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
1335
1336         }
1337 }
1338
1339 static void
1340 got_introspection (GUPnPServiceInfo          *info,
1341                    GUPnPServiceIntrospection *introspection,
1342                    const GError              *error,
1343                    gpointer                   user_data)
1344 {
1345         GUPnPService *service = GUPNP_SERVICE (info);
1346         const GList *state_variables, *l;
1347         GHashTableIter iter;
1348         gpointer data;
1349
1350         if (introspection) {
1351                 state_variables =
1352                         gupnp_service_introspection_list_state_variables
1353                                 (introspection);
1354
1355                 for (l = state_variables; l; l = l->next) {
1356                         GUPnPServiceStateVariableInfo *variable;
1357
1358                         variable = l->data;
1359
1360                         if (!variable->send_events)
1361                                 continue;
1362
1363                         service->priv->state_variables =
1364                                 g_list_prepend (service->priv->state_variables,
1365                                                 g_strdup (variable->name));
1366                 }
1367
1368                 g_object_unref (introspection);
1369         } else
1370                 g_warning ("Failed to get SCPD: %s\n"
1371                            "The initial event message will not be sent.",
1372                            error ? error->message : "No error");
1373
1374         g_hash_table_iter_init (&iter, service->priv->subscriptions);
1375
1376         while (g_hash_table_iter_next (&iter, NULL, &data)) {
1377                 send_initial_state ((SubscriptionData *) data);
1378                 if (subscription_data_can_delete ((SubscriptionData *) data))
1379                         g_hash_table_iter_remove (&iter);
1380         }
1381 }
1382
1383 static char *
1384 path_from_url (const char *url)
1385 {
1386         SoupURI *uri;
1387         gchar   *path;
1388
1389         uri = soup_uri_new (url);
1390         path = soup_uri_to_string (uri, TRUE);
1391         soup_uri_free (uri);
1392
1393         return path;
1394 }
1395
1396 static GObject *
1397 gupnp_service_constructor (GType                  type,
1398                            guint                  n_construct_params,
1399                            GObjectConstructParam *construct_params)
1400 {
1401         GObjectClass *object_class;
1402         GObject *object;
1403         GUPnPServiceInfo *info;
1404         GUPnPContext *context;
1405         SoupServer *server;
1406         char *url;
1407         char *path;
1408
1409         object_class = G_OBJECT_CLASS (gupnp_service_parent_class);
1410
1411         /* Construct */
1412         object = object_class->constructor (type,
1413                                             n_construct_params,
1414                                             construct_params);
1415
1416         info    = GUPNP_SERVICE_INFO (object);
1417
1418         /* Get introspection and save state variable names */
1419         gupnp_service_info_get_introspection_async (info,
1420                                                     got_introspection,
1421                                                     NULL);
1422
1423         /* Get server */
1424         context = gupnp_service_info_get_context (info);
1425         server = gupnp_context_get_server (context);
1426
1427         /* Run listener on controlURL */
1428         url = gupnp_service_info_get_control_url (info);
1429         path = path_from_url (url);
1430         soup_server_add_handler (server, path,
1431                                  control_server_handler, object, NULL);
1432         g_free (path);
1433         g_free (url);
1434
1435         /* Run listener on eventSubscriptionURL */
1436         url = gupnp_service_info_get_event_subscription_url (info);
1437         path = path_from_url (url);
1438         soup_server_add_handler (server, path,
1439                                  subscription_server_handler, object, NULL);
1440         g_free (path);
1441         g_free (url);
1442
1443         return object;
1444 }
1445
1446 /* Root device availability changed. */
1447 static void
1448 notify_available_cb (GObject *object,
1449                      GParamSpec *pspec,
1450                      gpointer    user_data)
1451 {
1452         GUPnPService *service;
1453
1454         service = GUPNP_SERVICE (user_data);
1455
1456         if (!gupnp_root_device_get_available (GUPNP_ROOT_DEVICE (object))) {
1457                 /* Root device now unavailable: Purge subscriptions */
1458                 g_hash_table_remove_all (service->priv->subscriptions);
1459         }
1460 }
1461
1462 static void
1463 gupnp_service_set_property (GObject      *object,
1464                             guint         property_id,
1465                             const GValue *value,
1466                             GParamSpec   *pspec)
1467 {
1468         GUPnPService *service;
1469
1470         service = GUPNP_SERVICE (object);
1471
1472         switch (property_id) {
1473         case PROP_ROOT_DEVICE: {
1474                 GUPnPRootDevice **dev;
1475
1476                 service->priv->root_device = g_value_get_object (value);
1477                 dev = &(service->priv->root_device);
1478
1479                 g_object_add_weak_pointer
1480                         (G_OBJECT (service->priv->root_device),
1481                          (gpointer *) dev);
1482
1483                 service->priv->notify_available_id =
1484                         g_signal_connect_object (service->priv->root_device,
1485                                                  "notify::available",
1486                                                  G_CALLBACK
1487                                                         (notify_available_cb),
1488                                                  object,
1489                                                  0);
1490
1491                 break;
1492         }
1493         default:
1494                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1495                 break;
1496         }
1497 }
1498
1499 static void
1500 gupnp_service_get_property (GObject    *object,
1501                             guint       property_id,
1502                             GValue     *value,
1503                             GParamSpec *pspec)
1504 {
1505         GUPnPService *service;
1506
1507         service = GUPNP_SERVICE (object);
1508
1509         switch (property_id) {
1510         case PROP_ROOT_DEVICE:
1511                 g_value_set_object (value, service->priv->root_device);
1512                 break;
1513         default:
1514                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1515                 break;
1516         }
1517 }
1518
1519 static void
1520 gupnp_service_dispose (GObject *object)
1521 {
1522         GUPnPService *service;
1523         GObjectClass *object_class;
1524         GUPnPServiceInfo *info;
1525         GUPnPContext *context;
1526         SoupServer *server;
1527         char *url;
1528         char *path;
1529
1530         service = GUPNP_SERVICE (object);
1531
1532         /* Get server */
1533         info = GUPNP_SERVICE_INFO (service);
1534         context = gupnp_service_info_get_context (info);
1535         server = gupnp_context_get_server (context);
1536
1537         /* Remove listener on controlURL */
1538         url = gupnp_service_info_get_control_url (info);
1539         path = path_from_url (url);
1540         soup_server_remove_handler (server, path);
1541         g_free (path);
1542         g_free (url);
1543
1544         /* Remove listener on eventSubscriptionURL */
1545         url = gupnp_service_info_get_event_subscription_url (info);
1546         path = path_from_url (url);
1547         soup_server_remove_handler (server, path);
1548         g_free (path);
1549         g_free (url);
1550
1551         if (service->priv->root_device) {
1552                 GUPnPRootDevice **dev = &(service->priv->root_device);
1553
1554                 if (g_signal_handler_is_connected
1555                         (service->priv->root_device,
1556                          service->priv->notify_available_id)) {
1557                         g_signal_handler_disconnect
1558                                 (service->priv->root_device,
1559                                  service->priv->notify_available_id);
1560                 }
1561
1562                 g_object_remove_weak_pointer
1563                         (G_OBJECT (service->priv->root_device),
1564                          (gpointer *) dev);
1565
1566                 service->priv->root_device = NULL;
1567         }
1568
1569         /* Cancel pending messages */
1570         g_hash_table_remove_all (service->priv->subscriptions);
1571
1572         /* Call super */
1573         object_class = G_OBJECT_CLASS (gupnp_service_parent_class);
1574         object_class->dispose (object);
1575 }
1576
1577 static void
1578 gupnp_service_finalize (GObject *object)
1579 {
1580         GUPnPService *service;
1581         GObjectClass *object_class;
1582         NotifyData *data;
1583
1584         service = GUPNP_SERVICE (object);
1585
1586         /* Free subscription hash */
1587         g_hash_table_destroy (service->priv->subscriptions);
1588
1589         /* Free state variable list */
1590         while (service->priv->state_variables) {
1591                 g_free (service->priv->state_variables->data);
1592                 service->priv->state_variables =
1593                         g_list_delete_link (service->priv->state_variables,
1594                                             service->priv->state_variables);
1595         }
1596
1597         /* Free notify queue */
1598         while ((data = g_queue_pop_head (service->priv->notify_queue)))
1599                 notify_data_free (data);
1600
1601         g_queue_free (service->priv->notify_queue);
1602
1603         if (service->priv->session) {
1604                 g_object_unref (service->priv->session);
1605                 service->priv->session = NULL;
1606         }
1607
1608         /* Call super */
1609         object_class = G_OBJECT_CLASS (gupnp_service_parent_class);
1610         object_class->finalize (object);
1611 }
1612
1613 static void
1614 gupnp_service_class_init (GUPnPServiceClass *klass)
1615 {
1616         GObjectClass *object_class;
1617
1618         object_class = G_OBJECT_CLASS (klass);
1619
1620         object_class->set_property = gupnp_service_set_property;
1621         object_class->get_property = gupnp_service_get_property;
1622         object_class->constructor  = gupnp_service_constructor;
1623         object_class->dispose      = gupnp_service_dispose;
1624         object_class->finalize     = gupnp_service_finalize;
1625
1626         g_type_class_add_private (klass, sizeof (GUPnPServicePrivate));
1627
1628         /**
1629          * GUPnPService:root-device:
1630          *
1631          * The containing #GUPnPRootDevice.
1632          **/
1633         g_object_class_install_property
1634                 (object_class,
1635                  PROP_ROOT_DEVICE,
1636                  g_param_spec_object ("root-device",
1637                                       "Root device",
1638                                       "The GUPnPRootDevice",
1639                                       GUPNP_TYPE_ROOT_DEVICE,
1640                                       G_PARAM_READWRITE |
1641                                       G_PARAM_CONSTRUCT_ONLY |
1642                                       G_PARAM_STATIC_NAME |
1643                                       G_PARAM_STATIC_NICK |
1644                                       G_PARAM_STATIC_BLURB));
1645
1646         /**
1647          * GUPnPService::action-invoked:
1648          * @service: The #GUPnPService that received the signal
1649          * @action: The invoked #GUPnPAction
1650          *
1651          * Emitted whenever an action is invoked. Handler should process
1652          * @action and must call either gupnp_service_action_return() or
1653          * gupnp_service_action_return_error().
1654          **/
1655         signals[ACTION_INVOKED] =
1656                 g_signal_new ("action-invoked",
1657                               GUPNP_TYPE_SERVICE,
1658                               G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
1659                               G_STRUCT_OFFSET (GUPnPServiceClass,
1660                                                action_invoked),
1661                               NULL,
1662                               NULL,
1663                               g_cclosure_marshal_VOID__BOXED,
1664                               G_TYPE_NONE,
1665                               1,
1666                               GUPNP_TYPE_SERVICE_ACTION);
1667
1668         /**
1669          * GUPnPService::query-variable:
1670          * @service: The #GUPnPService that received the signal
1671          * @variable: The variable that is being queried
1672          * @value: The location of the #GValue of the variable
1673          *
1674          * Emitted whenever @service needs to know the value of @variable.
1675          * Handler should fill @value with the value of @variable.
1676          **/
1677         signals[QUERY_VARIABLE] =
1678                 g_signal_new ("query-variable",
1679                               GUPNP_TYPE_SERVICE,
1680                               G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
1681                               G_STRUCT_OFFSET (GUPnPServiceClass,
1682                                                query_variable),
1683                               NULL,
1684                               NULL,
1685                               gupnp_marshal_VOID__STRING_POINTER,
1686                               G_TYPE_NONE,
1687                               2,
1688                               G_TYPE_STRING,
1689                               G_TYPE_POINTER /* Not G_TYPE_VALUE as this
1690                                                 is an outward argument! */);
1691
1692         /**
1693          * GUPnPService::notify-failed:
1694          * @service: The #GUPnPService that received the signal
1695          * @callback_url: The callback URL
1696          * @reason: A pointer to a #GError describing why the notify failed
1697          *
1698          * Emitted whenever notification of a client fails.
1699          **/
1700         signals[NOTIFY_FAILED] =
1701                 g_signal_new ("notify-failed",
1702                               GUPNP_TYPE_SERVICE,
1703                               G_SIGNAL_RUN_LAST,
1704                               G_STRUCT_OFFSET (GUPnPServiceClass,
1705                                                notify_failed),
1706                               NULL,
1707                               NULL,
1708                               gupnp_marshal_VOID__POINTER_POINTER,
1709                               G_TYPE_NONE,
1710                               2,
1711                               G_TYPE_POINTER,
1712                               G_TYPE_POINTER);
1713 }
1714
1715 /**
1716  * gupnp_service_notify:
1717  * @service: A #GUPnPService
1718  * @Varargs: Tuples of variable name, variable type, and variable value,
1719  * terminated with %NULL.
1720  *
1721  * Notifies listening clients that the properties listed in @Varargs
1722  * have changed to the specified values.
1723  **/
1724 void
1725 gupnp_service_notify (GUPnPService *service,
1726                       ...)
1727 {
1728         va_list var_args;
1729
1730         g_return_if_fail (GUPNP_IS_SERVICE (service));
1731
1732         va_start (var_args, service);
1733         gupnp_service_notify_valist (service, var_args);
1734         va_end (var_args);
1735 }
1736
1737 /**
1738  * gupnp_service_notify_valist:
1739  * @service: A #GUPnPService
1740  * @var_args: A va_list of tuples of variable name, variable type, and variable
1741  * value, terminated with %NULL.
1742  *
1743  * See gupnp_service_notify(); this version takes a va_list for
1744  * use by language bindings.
1745  **/
1746 void
1747 gupnp_service_notify_valist (GUPnPService *service,
1748                              va_list       var_args)
1749 {
1750         const char *var_name;
1751         GType var_type;
1752         GValue value = {0, };
1753         char *collect_error;
1754
1755         g_return_if_fail (GUPNP_IS_SERVICE (service));
1756
1757         collect_error = NULL;
1758
1759         var_name = va_arg (var_args, const char *);
1760         while (var_name) {
1761                 var_type = va_arg (var_args, GType);
1762                 g_value_init (&value, var_type);
1763
1764                 G_VALUE_COLLECT (&value, var_args, G_VALUE_NOCOPY_CONTENTS,
1765                                  &collect_error);
1766                 if (!collect_error) {
1767                         gupnp_service_notify_value (service, var_name, &value);
1768
1769                         g_value_unset (&value);
1770
1771                 } else {
1772                         g_warning ("Error collecting value: %s\n",
1773                                    collect_error);
1774
1775                         g_free (collect_error);
1776                 }
1777
1778                 var_name = va_arg (var_args, const char *);
1779         }
1780 }
1781
1782 /* Received notify response. */
1783 static void
1784 notify_got_response (SoupSession *session,
1785                      SoupMessage *msg,
1786                      gpointer     user_data)
1787 {
1788         SubscriptionData *data;
1789
1790         /* Cancelled? */
1791         if (msg->status_code == SOUP_STATUS_CANCELLED)
1792                 return;
1793
1794         data = user_data;
1795
1796         /* Remove from pending messages list */
1797         data->pending_messages = g_list_remove (data->pending_messages, msg);
1798
1799         if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
1800                 data->initial_state_sent = TRUE;
1801
1802                 /* Success: reset callbacks pointer */
1803                 data->callbacks = g_list_first (data->callbacks);
1804
1805         } else if (msg->status_code == SOUP_STATUS_PRECONDITION_FAILED) {
1806                 /* Precondition failed: Cancel subscription */
1807                 g_hash_table_remove (data->service->priv->subscriptions,
1808                                      data->sid);
1809
1810         } else {
1811                 /* Other failure: Try next callback or signal failure. */
1812                 if (data->callbacks->next) {
1813                         SoupURI *uri;
1814                         SoupSession *service_session;
1815
1816                         /* Call next callback */
1817                         data->callbacks = data->callbacks->next;
1818
1819                         uri = soup_uri_new (data->callbacks->data);
1820                         soup_message_set_uri (msg, uri);
1821                         soup_uri_free (uri);
1822
1823                         /* And re-queue */
1824                         data->pending_messages = 
1825                                 g_list_prepend (data->pending_messages, msg);
1826
1827                         service_session = gupnp_service_get_session (data->service);
1828
1829                         soup_session_requeue_message (service_session, msg);
1830                 } else {
1831                         /* Emit 'notify-failed' signal */
1832                         GError *error;
1833
1834                         error = g_error_new_literal
1835                                         (GUPNP_EVENTING_ERROR,
1836                                          GUPNP_EVENTING_ERROR_NOTIFY_FAILED,
1837                                          msg->reason_phrase);
1838
1839                         g_signal_emit (data->service,
1840                                        signals[NOTIFY_FAILED],
1841                                        0,
1842                                        data->callbacks,
1843                                        error);
1844
1845                         g_error_free (error);
1846
1847                         /* Reset callbacks pointer */
1848                         data->callbacks = g_list_first (data->callbacks);
1849                 }
1850         }
1851 }
1852
1853 /* Send notification @user_data to subscriber @value */
1854 static void
1855 notify_subscriber (gpointer key,
1856                    gpointer value,
1857                    gpointer user_data)
1858 {
1859         SubscriptionData *data;
1860         const char *property_set;
1861         char *tmp;
1862         SoupMessage *msg;
1863         SoupSession *session;
1864
1865         data = value;
1866         property_set = user_data;
1867
1868         /* Subscriber called unsubscribe */
1869         if (subscription_data_can_delete (data))
1870                 return;
1871
1872         /* Create message */
1873         msg = soup_message_new (GENA_METHOD_NOTIFY, data->callbacks->data);
1874         if (!msg) {
1875                 g_warning ("Invalid callback URL: %s",
1876                            (char *) data->callbacks->data);
1877
1878                 return;
1879         }
1880
1881         soup_message_headers_append (msg->request_headers,
1882                                      "NT",
1883                                      "upnp:event");
1884
1885         soup_message_headers_append (msg->request_headers,
1886                                      "NTS",
1887                                      "upnp:propchange");
1888
1889         soup_message_headers_append (msg->request_headers,
1890                                      "SID",
1891                                      data->sid);
1892
1893         tmp = g_strdup_printf ("%d", data->seq);
1894         soup_message_headers_append (msg->request_headers,
1895                                      "SEQ",
1896                                      tmp);
1897         g_free (tmp);
1898
1899         /* Handle overflow */
1900         if (data->seq < G_MAXINT32)
1901                 data->seq++;
1902         else
1903                 data->seq = 1;
1904
1905         /* Add body */
1906         soup_message_set_request (msg,
1907                                   "text/xml; charset=\"utf-8\"",
1908                                   SOUP_MEMORY_TAKE,
1909                                   g_strdup (property_set),
1910                                   strlen (property_set));
1911
1912         /* Queue */
1913         data->pending_messages = g_list_prepend (data->pending_messages, msg);
1914
1915         session = gupnp_service_get_session (data->service);
1916
1917         soup_session_queue_message (session,
1918                                     msg,
1919                                     notify_got_response,
1920                                     data);
1921 }
1922
1923 /* Create a property set from @queue */
1924 static char *
1925 create_property_set (GQueue *queue)
1926 {
1927         NotifyData *data;
1928         GString *str;
1929
1930         /* Compose property set */
1931         str = xml_util_new_string ();
1932
1933         g_string_append (str,
1934                          "<?xml version=\"1.0\"?>"
1935                          "<e:propertyset xmlns:e="
1936                                 "\"urn:schemas-upnp-org:event-1-0\">");
1937
1938         /* Add variables */
1939         while ((data = g_queue_pop_head (queue))) {
1940                 xml_util_start_element (str, "e:property");
1941                 xml_util_start_element (str, data->variable);
1942                 gvalue_util_value_append_to_xml_string (&data->value, str);
1943                 xml_util_end_element (str, data->variable);
1944                 xml_util_end_element (str, "e:property");
1945
1946                 /* Cleanup */
1947                 notify_data_free (data);
1948         }
1949
1950         g_string_append (str, "</e:propertyset>");
1951
1952         /* Cleanup & return */
1953         return g_string_free (str, FALSE);
1954 }
1955
1956 /* Flush all queued notifications */
1957 static void
1958 flush_notifications (GUPnPService *service)
1959 {
1960         char *mem;
1961
1962         /* Create property set */
1963         mem = create_property_set (service->priv->notify_queue);
1964
1965         /* And send it off */
1966         g_hash_table_foreach (service->priv->subscriptions,
1967                               notify_subscriber,
1968                               mem);
1969
1970         /* Cleanup */
1971         g_free (mem);
1972 }
1973
1974 /**
1975  * gupnp_service_notify_value:
1976  * @service: A #GUPnPService
1977  * @variable: The name of the variable to notify
1978  * @value: The value of the variable
1979  *
1980  * Notifies listening clients that @variable has changed to @value.
1981  **/
1982 void
1983 gupnp_service_notify_value (GUPnPService *service,
1984                             const char   *variable,
1985                             const GValue *value)
1986 {
1987         NotifyData *data;
1988
1989         g_return_if_fail (GUPNP_IS_SERVICE (service));
1990         g_return_if_fail (variable != NULL);
1991         g_return_if_fail (G_IS_VALUE (value));
1992
1993         /* Queue */
1994         data = g_slice_new0 (NotifyData);
1995
1996         data->variable = g_strdup (variable);
1997
1998         g_value_init (&data->value, G_VALUE_TYPE (value));
1999         g_value_copy (value, &data->value);
2000
2001         g_queue_push_tail (service->priv->notify_queue, data);
2002
2003         /* And flush, if not frozen */
2004         if (!service->priv->notify_frozen)
2005                 flush_notifications (service);
2006 }
2007
2008 /**
2009  * gupnp_service_freeze_notify:
2010  * @service: A #GUPnPService
2011  *
2012  * Causes new notifications to be queued up until gupnp_service_thaw_notify()
2013  * is called.
2014  **/
2015 void
2016 gupnp_service_freeze_notify (GUPnPService *service)
2017 {
2018         g_return_if_fail (GUPNP_IS_SERVICE (service));
2019
2020         service->priv->notify_frozen = TRUE;
2021 }
2022
2023 /**
2024  * gupnp_service_thaw_notify:
2025  * @service: A #GUPnPService
2026  *
2027  * Sends out any pending notifications, and stops queuing of new ones.
2028  **/
2029 void
2030 gupnp_service_thaw_notify (GUPnPService *service)
2031 {
2032         g_return_if_fail (GUPNP_IS_SERVICE (service));
2033
2034         service->priv->notify_frozen = FALSE;
2035
2036         if (g_queue_get_length (service->priv->notify_queue) == 0)
2037                 return; /* Empty notify queue */
2038
2039         flush_notifications (service);
2040 }
2041
2042 /* Convert a CamelCase string to a lowercase string with underscores */
2043 static char *
2044 strip_camel_case (char *camel_str)
2045 {
2046         char *stripped;
2047         unsigned int i, j;
2048
2049         /* Keep enough space for underscores */
2050         stripped = g_malloc (strlen (camel_str) * 2);
2051
2052         for (i = 0, j = 0; i <= strlen (camel_str); i++) {
2053                 /* Convert every upper case letter to lower case and unless
2054                  * it's the first character, the last charachter, in the
2055                  * middle of an abbreviation or there is already an underscore
2056                  * before it, add an underscore before it */
2057                 if (g_ascii_isupper (camel_str[i])) {
2058                         if (i != 0 &&
2059                             camel_str[i + 1] != '\0' &&
2060                             camel_str[i - 1] != '_' &&
2061                             !g_ascii_isupper (camel_str[i - 1])) {
2062                                 stripped[j++] = '_';
2063                         }
2064                         stripped[j++] = g_ascii_tolower (camel_str[i]);
2065                 } else
2066                         stripped[j++] = camel_str[i];
2067         }
2068
2069         return stripped;
2070 }
2071
2072 static GCallback
2073 find_callback_by_name (GModule    *module,
2074                        const char *name)
2075 {
2076         GCallback callback;
2077         char *full_name;
2078
2079         /* First try with 'on_' prefix */
2080         full_name = g_strjoin ("_",
2081                                "on",
2082                                name,
2083                                NULL);
2084
2085         if (!g_module_symbol (module,
2086                               full_name,
2087                               (gpointer) &callback)) {
2088                 g_free (full_name);
2089
2090                 /* Now try with '_cb' postfix */
2091                 full_name = g_strjoin ("_",
2092                                        name,
2093                                        "cb",
2094                                        NULL);
2095
2096                 if (!g_module_symbol (module,
2097                                       full_name,
2098                                       (gpointer) &callback))
2099                         callback = NULL;
2100         }
2101
2102         g_free (full_name);
2103
2104         return callback;
2105 }
2106
2107 /* Use the strings from @name_list as details to @signal_name, and connect
2108  * callbacks with names based on these same strings to @signal_name::string. */
2109 static void
2110 connect_names_to_signal_handlers (GUPnPService *service,
2111                                   GModule      *module,
2112                                   const GList  *name_list,
2113                                   const char   *signal_name,
2114                                   const char   *callback_prefix,
2115                                   gpointer      user_data)
2116 {
2117         const GList *name_node;
2118
2119         for (name_node = name_list;
2120              name_node;
2121              name_node = name_node->next) {
2122                 GCallback callback;
2123                 char     *callback_name;
2124                 char     *signal_detail;
2125
2126                 signal_detail = (char *) name_node->data;
2127                 callback_name = strip_camel_case (signal_detail);
2128
2129                 if (callback_prefix) {
2130                         char *tmp;
2131
2132                         tmp = g_strjoin ("_",
2133                                          callback_prefix,
2134                                          callback_name,
2135                                          NULL);
2136
2137                         g_free (callback_name);
2138                         callback_name = tmp;
2139                 }
2140
2141                 callback = find_callback_by_name (module, callback_name);
2142                 g_free (callback_name);
2143
2144                 if (callback == NULL)
2145                         continue;
2146
2147                 signal_detail = g_strjoin ("::",
2148                                            signal_name,
2149                                            signal_detail,
2150                                            NULL);
2151
2152                 g_signal_connect (service,
2153                                   signal_detail,
2154                                   callback,
2155                                   user_data);
2156
2157                 g_free (signal_detail);
2158         }
2159 }
2160
2161 /**
2162  * gupnp_service_signals_autoconnect:
2163  * @service: A #GUPnPService
2164  * @user_data: the data to pass to each of the callbacks
2165  * @error: return location for a #GError, or %NULL
2166  *
2167  * A convenience function that attempts to connect all possible
2168  * #GUPnPService::action-invoked and #GUPnPService::query-variable signals to
2169  * appropriate callbacks for the service @service. It uses service introspection
2170  * and GModule's introspective features. It is very simillar to
2171  * glade_xml_signal_autoconnect() except that it attempts to guess the names of
2172  * the signal handlers on its own.
2173  *
2174  * For this function to do its magic, the application must name the callback
2175  * functions for #GUPnPService::action-invoked signals by striping the CamelCase
2176  * off the action names and either prepend "on_" or append "_cb" to them. Same
2177  * goes for #GUPnPService::query-variable signals, except that "query_" should
2178  * be prepended to the variable name. For example, callback function for
2179  * "GetSystemUpdateID" action should be either named as
2180  * "get_system_update_id_cb" or "on_get_system_update_id" and callback function
2181  * for the query of "SystemUpdateID" state variable should be named
2182  * "query_system_update_id_cb" or "on_query_system_update_id".
2183  *
2184  * Note that this function will not work correctly if GModule is not supported
2185  * on the platform or introspection is not available for service @service.
2186  *
2187  * WARNING: This function can not and therefore does not guarantee that the
2188  * resulting signal connections will be correct as it depends heavily on a
2189  * particular naming schemes described above.
2190  **/
2191 void
2192 gupnp_service_signals_autoconnect (GUPnPService *service,
2193                                    gpointer      user_data,
2194                                    GError      **error)
2195 {
2196         GUPnPServiceIntrospection *introspection;
2197         const GList               *names;
2198         GModule                   *module;
2199
2200         g_return_if_fail (GUPNP_IS_SERVICE (service));
2201
2202         introspection = gupnp_service_info_get_introspection
2203                                 (GUPNP_SERVICE_INFO (service),
2204                                  error);
2205         if (!introspection)
2206                 return;
2207
2208         /* Get a handle on the main executable -- use this to find symbols */
2209         module = g_module_open (NULL, 0);
2210         if (module == NULL) {
2211                 g_error ("Failed to open module: %s", g_module_error ());
2212
2213                 g_object_unref (introspection);
2214
2215                 return;
2216         }
2217
2218         names = gupnp_service_introspection_list_action_names (introspection);
2219         connect_names_to_signal_handlers (service,
2220                                           module,
2221                                           names,
2222                                           "action-invoked",
2223                                           NULL,
2224                                           user_data);
2225
2226         names = gupnp_service_introspection_list_state_variable_names
2227                         (introspection);
2228         connect_names_to_signal_handlers (service,
2229                                           module,
2230                                           names,
2231                                           "query-variable",
2232                                           "query",
2233                                           user_data);
2234
2235         g_module_close (module);
2236         g_object_unref (introspection);
2237 }