gupnp-service-proxy.c 73.8 KB
Newer Older
1
/*
2
 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 *
 * Author: Jorn Baayen <jorn@openedhand.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

22
23
24
25
26
/**
 * SECTION:gupnp-service-proxy
 * @short_description: Proxy class for remote services.
 *
 * #GUPnPServiceProxy sends commands to a remote UPnP service and handles
27
 * incoming event notifications. #GUPnPServiceProxy implements the
28
29
30
 * #GUPnPServiceInfo interface.
 */

31
#include <libsoup/soup.h>
32
#include <gobject/gvaluecollector.h>
Jorn Baayen's avatar
Jorn Baayen committed
33
#include <string.h>
34
#include <locale.h>
Jorn Baayen's avatar
Jorn Baayen committed
35

36
#include "gupnp-service-proxy.h"
37
#include "gupnp-context-private.h"
38
#include "gupnp-error.h"
39
#include "gupnp-error-private.h"
40
#include "gupnp-types.h"
Jorn Baayen's avatar
Jorn Baayen committed
41
#include "xml-util.h"
42
#include "gena-protocol.h"
43
#include "http-headers.h"
44
#include "gvalue-util.h"
Jorn Baayen's avatar
Jorn Baayen committed
45

Jorn Baayen's avatar
Jorn Baayen committed
46
47
48
G_DEFINE_TYPE (GUPnPServiceProxy,
               gupnp_service_proxy,
               GUPNP_TYPE_SERVICE_INFO);
Jorn Baayen's avatar
Jorn Baayen committed
49
50

struct _GUPnPServiceProxyPrivate {
Jorn Baayen's avatar
Jorn Baayen committed
51
        gboolean subscribed;
Jorn Baayen's avatar
Jorn Baayen committed
52
53

        GList *pending_actions;
54

55
        char *path; /* Path to this proxy */
56

57
        char *sid; /* Subscription ID */
58
        GSource *subscription_timeout_src;
59
60

        int seq; /* Event sequence number */
61
62

        GHashTable *notify_hash;
63
64

        GList *pending_messages; /* Pending SoupMessages from this proxy */
65

66
67
        GList *pending_notifies; /* Pending notifications to be sent (xmlDoc) */
        GSource *notify_idle_src; /* Idle handler src of notification emiter */
Jorn Baayen's avatar
Jorn Baayen committed
68
69
70
71
72
};

enum {
        PROP_0,
        PROP_SUBSCRIBED
Jorn Baayen's avatar
Jorn Baayen committed
73
74
};

Jorn Baayen's avatar
Jorn Baayen committed
75
76
77
78
79
80
81
enum {
        SUBSCRIPTION_LOST,
        LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

82
83
84
struct _GUPnPServiceProxyAction {
        GUPnPServiceProxy *proxy;

85
        SoupMessage *msg;
86
        GString *msg_str;
87
88
89

        GUPnPServiceProxyActionCallback callback;
        gpointer user_data;
Jorn Baayen's avatar
Jorn Baayen committed
90

91
92
93
        GError *error;    /* If non-NULL, description of error that
                             occurred when preparing message */

94
95
        va_list var_args; /* The va_list after begin_action_valist has
                             gone through it. Used by send_action_valist(). */
96
97
};

98
99
100
101
102
103
104
105
106
107
108
typedef struct {
        GType type;

        GList *callbacks;
} NotifyData;

typedef struct {
        GUPnPServiceProxyNotifyCallback callback;
        gpointer user_data;
} CallbackData;

Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
109
110
typedef struct {
        char *sid;
111
        int seq;
Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
112
113
114
115

        xmlDoc *doc;
} EmitNotifyData;

116
static void
117
118
subscribe_got_response (SoupSession       *session,
                        SoupMessage       *msg,
119
                        GUPnPServiceProxy *proxy);
120
static void
121
122
subscribe (GUPnPServiceProxy *proxy);
static void
123
unsubscribe (GUPnPServiceProxy *proxy);
124

125
126
127
128
129
130
static void
gupnp_service_proxy_action_free (GUPnPServiceProxyAction *action)
{
        action->proxy->priv->pending_actions =
                g_list_remove (action->proxy->priv->pending_actions, action);

131
132
        if (action->msg != NULL)
                g_object_unref (action->msg);
133

134
135
136
        g_slice_free (GUPnPServiceProxyAction, action);
}

137
138
139
140
141
142
143
144
static void
notify_data_free (NotifyData *data)
{
        g_list_free (data->callbacks);

        g_slice_free (NotifyData, data);
}

Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
145
146
147
/* Steals doc reference */
static EmitNotifyData *
emit_notify_data_new (const char *sid,
148
                      int         seq,
Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
149
150
151
152
153
154
155
                      xmlDoc     *doc)
{
        EmitNotifyData *data;

        data = g_slice_new (EmitNotifyData);

        data->sid = g_strdup (sid);
156
        data->seq = seq;
Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
157
158
159
160
161
162
163
164
165
166
167
168
169
170
        data->doc = doc;

        return data;
}

static void
emit_notify_data_free (EmitNotifyData *data)
{
        g_free (data->sid);
        xmlFreeDoc (data->doc);

        g_slice_free (EmitNotifyData, data);
}

Jorn Baayen's avatar
Jorn Baayen committed
171
172
173
static void
gupnp_service_proxy_init (GUPnPServiceProxy *proxy)
{
174
175
        static int proxy_counter = 0;

Jorn Baayen's avatar
Jorn Baayen committed
176
177
178
        proxy->priv = G_TYPE_INSTANCE_GET_PRIVATE (proxy,
                                                   GUPNP_TYPE_SERVICE_PROXY,
                                                   GUPnPServiceProxyPrivate);
179
180
181
182

        /* Generate unique path */
        proxy->priv->path = g_strdup_printf ("/ServiceProxy%d", proxy_counter);
        proxy_counter++;
183
184
185
186
187
188
189

        /* Set up notify hash */
        proxy->priv->notify_hash =
                g_hash_table_new_full (g_str_hash,
                                       g_str_equal,
                                       g_free,
                                       (GDestroyNotify) notify_data_free);
Jorn Baayen's avatar
Jorn Baayen committed
190
191
}

Jorn Baayen's avatar
Jorn Baayen committed
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
static void
gupnp_service_proxy_set_property (GObject      *object,
                                  guint         property_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
        GUPnPServiceProxy *proxy;

        proxy = GUPNP_SERVICE_PROXY (object);

        switch (property_id) {
        case PROP_SUBSCRIBED:
                gupnp_service_proxy_set_subscribed
                                (proxy, g_value_get_boolean (value));
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

static void
gupnp_service_proxy_get_property (GObject    *object,
                                  guint       property_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
        GUPnPServiceProxy *proxy;

        proxy = GUPNP_SERVICE_PROXY (object);

        switch (property_id) {
        case PROP_SUBSCRIBED:
                g_value_set_boolean (value,
                                     proxy->priv->subscribed);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

static void
gupnp_service_proxy_dispose (GObject *object)
{
        GUPnPServiceProxy *proxy;
238
        GObjectClass *object_class;
239
240
        GUPnPContext *context;
        SoupSession *session;
Jorn Baayen's avatar
Jorn Baayen committed
241
242
243

        proxy = GUPNP_SERVICE_PROXY (object);

244
245
246
247
248
249
250
        /* Unsubscribe */
        if (proxy->priv->subscribed) {
                unsubscribe (proxy);

                proxy->priv->subscribed = FALSE;
        }

251
        /* Cancel pending actions */
Jorn Baayen's avatar
Jorn Baayen committed
252
253
254
255
        while (proxy->priv->pending_actions) {
                GUPnPServiceProxyAction *action;

                action = proxy->priv->pending_actions->data;
256

Jorn Baayen's avatar
Jorn Baayen committed
257
258
                gupnp_service_proxy_cancel_action (proxy, action);
        }
259

260
261
262
        /* Cancel pending messages */
        context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
        if (context)
263
                session = gupnp_context_get_session (context);
264
265
266
267
268
269
270
271
        else
                session = NULL; /* Not the first time dispose is called. */

        while (proxy->priv->pending_messages) {
                SoupMessage *msg;

                msg = proxy->priv->pending_messages->data;

272
273
274
                soup_session_cancel_message (session,
                                             msg,
                                             SOUP_STATUS_CANCELLED);
275
276
277
278
279
280

                proxy->priv->pending_messages =
                        g_list_delete_link (proxy->priv->pending_messages,
                                            proxy->priv->pending_messages);
        }

281
        /* Cancel pending notifications */
282
283
284
        if (proxy->priv->notify_idle_src) {
                g_source_destroy (proxy->priv->notify_idle_src);
                proxy->priv->notify_idle_src = NULL;
285
        }
286
        
287
        while (proxy->priv->pending_notifies) {
288
                emit_notify_data_free (proxy->priv->pending_notifies->data);
289
290
291
                proxy->priv->pending_notifies =
                        g_list_delete_link (proxy->priv->pending_notifies,
                                            proxy->priv->pending_notifies);
292
293
        }
        
294
295
296
        /* Call super */
        object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class);
        object_class->dispose (object);
Jorn Baayen's avatar
Jorn Baayen committed
297
298
}

299
300
301
302
static void
gupnp_service_proxy_finalize (GObject *object)
{
        GUPnPServiceProxy *proxy;
303
        GObjectClass *object_class;
304
305
306

        proxy = GUPNP_SERVICE_PROXY (object);

307
        g_free (proxy->priv->path);
308
309

        g_hash_table_destroy (proxy->priv->notify_hash);
310
311
312
313

        /* Call super */
        object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class);
        object_class->finalize (object);
314
315
}

Jorn Baayen's avatar
Jorn Baayen committed
316
317
318
319
320
321
322
static void
gupnp_service_proxy_class_init (GUPnPServiceProxyClass *klass)
{
        GObjectClass *object_class;

        object_class = G_OBJECT_CLASS (klass);

Jorn Baayen's avatar
Jorn Baayen committed
323
324
325
        object_class->set_property = gupnp_service_proxy_set_property;
        object_class->get_property = gupnp_service_proxy_get_property;
        object_class->dispose      = gupnp_service_proxy_dispose;
326
        object_class->finalize     = gupnp_service_proxy_finalize;
Jorn Baayen's avatar
Jorn Baayen committed
327

Jorn Baayen's avatar
Jorn Baayen committed
328
        g_type_class_add_private (klass, sizeof (GUPnPServiceProxyPrivate));
Jorn Baayen's avatar
Jorn Baayen committed
329

330
        /**
331
         * GUPnPServiceProxy:subscribed:
332
333
334
         *
         * Whether we are subscribed to this service.
         **/
Jorn Baayen's avatar
Jorn Baayen committed
335
336
337
338
339
        g_object_class_install_property
                (object_class,
                 PROP_SUBSCRIBED,
                 g_param_spec_boolean ("subscribed",
                                       "Subscribed",
340
                                       "Whether we are subscribed to this "
Jorn Baayen's avatar
Jorn Baayen committed
341
342
                                       "service",
                                       FALSE,
343
344
345
346
                                       G_PARAM_READWRITE |
                                       G_PARAM_STATIC_NAME |
                                       G_PARAM_STATIC_NICK |
                                       G_PARAM_STATIC_BLURB));
347

348
        /**
349
         * GUPnPServiceProxy::subscription-lost:
350
351
352
353
354
355
356
         * @proxy: The #GUPnPServiceProxy that received the signal
         * @error: A pointer to a #GError describing why the subscription has
         * been lost
         *
         * Emitted whenever the subscription to this service has been lost due
         * to an error condition.
         **/
Jorn Baayen's avatar
Jorn Baayen committed
357
358
359
360
361
362
363
364
365
366
367
368
        signals[SUBSCRIPTION_LOST] =
                g_signal_new ("subscription-lost",
                              GUPNP_TYPE_SERVICE_PROXY,
                              G_SIGNAL_RUN_LAST,
                              G_STRUCT_OFFSET (GUPnPServiceProxyClass,
                                               subscription_lost),
                              NULL,
                              NULL,
                              g_cclosure_marshal_VOID__POINTER,
                              G_TYPE_NONE,
                              1,
                              G_TYPE_POINTER);
Jorn Baayen's avatar
Jorn Baayen committed
369
370
}

Jorn Baayen's avatar
Jorn Baayen committed
371
/**
372
 * gupnp_service_proxy_send_action:
Jorn Baayen's avatar
Jorn Baayen committed
373
374
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
375
 * @error: The location where to store any error, or %NULL
376
377
 * @Varargs: tuples of in parameter name, in parameter type, and in parameter
 * value, followed by %NULL, and then tuples of out parameter name,
378
 * out parameter type, and out parameter value location, terminated with %NULL
Jorn Baayen's avatar
Jorn Baayen committed
379
380
 *
 * Sends action @action with parameters @Varargs to the service exposed by
381
382
 * @proxy synchronously. If an error occurred, @error will be set. In case of
 * a UPnPError the error code will be the same in @error.
Jorn Baayen's avatar
Jorn Baayen committed
383
 *
384
 * Return value: %TRUE if sending the action was succesful.
Jorn Baayen's avatar
Jorn Baayen committed
385
386
387
388
389
390
391
 **/
gboolean
gupnp_service_proxy_send_action (GUPnPServiceProxy *proxy,
                                 const char        *action,
                                 GError           **error,
                                 ...)
{
Jorn Baayen's avatar
Jorn Baayen committed
392
393
        va_list var_args;
        gboolean ret;
Jorn Baayen's avatar
Jorn Baayen committed
394

Jorn Baayen's avatar
Jorn Baayen committed
395
396
397
398
399
400
401
402
        va_start (var_args, error);
        ret = gupnp_service_proxy_send_action_valist (proxy,
                                                      action,
                                                      error,
                                                      var_args);
        va_end (var_args);

        return ret;
Jorn Baayen's avatar
Jorn Baayen committed
403
404
}

Jorn Baayen's avatar
Jorn Baayen committed
405
406
407
408
409
410
411
412
static void
stop_main_loop (GUPnPServiceProxy       *proxy,
                GUPnPServiceProxyAction *action,
                gpointer                 user_data)
{
        g_main_loop_quit ((GMainLoop *) user_data);
}

Jorn Baayen's avatar
Jorn Baayen committed
413
/**
414
 * gupnp_service_proxy_send_action_valist:
Jorn Baayen's avatar
Jorn Baayen committed
415
416
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
417
 * @error: The location where to store any error, or %NULL
418
419
 * @var_args: va_list of tuples of in parameter name, in parameter type, and in
 * parameter value, followed by %NULL, and then tuples of out parameter name,
Jorn Baayen's avatar
Jorn Baayen committed
420
421
422
423
424
 * out parameter type, and out parameter value location
 *
 * See gupnp_service_proxy_send_action(); this version takes a va_list for
 * use by language bindings.
 *
425
 * Return value: %TRUE if sending the action was succesful.
Jorn Baayen's avatar
Jorn Baayen committed
426
427
428
429
430
431
432
 **/
gboolean
gupnp_service_proxy_send_action_valist (GUPnPServiceProxy *proxy,
                                        const char        *action,
                                        GError           **error,
                                        va_list            var_args)
{
Jorn Baayen's avatar
Jorn Baayen committed
433
434
435
436
        GUPnPContext *context;
        GMainContext *main_context;
        GMainLoop *main_loop;
        GUPnPServiceProxyAction *handle;
437

Jorn Baayen's avatar
Jorn Baayen committed
438
439
440
        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
        g_return_val_if_fail (action, FALSE);

Jorn Baayen's avatar
Jorn Baayen committed
441
442
        context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
        main_context = gssdp_client_get_main_context (GSSDP_CLIENT (context));
443
        main_loop = g_main_loop_new (main_context, TRUE);
Jorn Baayen's avatar
Jorn Baayen committed
444
445
446
447
448
449
450
451

        handle = gupnp_service_proxy_begin_action_valist (proxy,
                                                          action,
                                                          stop_main_loop,
                                                          main_loop,
                                                          var_args);

        /* Loop till we get a reply (or time out) */
452
453
        if (g_main_loop_is_running (main_loop))
                g_main_loop_run (main_loop);
Jorn Baayen's avatar
Jorn Baayen committed
454
455
456
457
458
459

        g_main_loop_unref (main_loop);

        if (!gupnp_service_proxy_end_action_valist (proxy,
                                                    handle,
                                                    error,
460
                                                    handle->var_args))
Jorn Baayen's avatar
Jorn Baayen committed
461
462
463
                return FALSE;

        return TRUE;
Jorn Baayen's avatar
Jorn Baayen committed
464
465
}

466
/**
467
 * gupnp_service_proxy_send_action_hash:
468
469
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
470
 * @error: The location where to store any error, or %NULL
471
472
473
474
 * @in_hash: (element-type utf8 GValue) (transfer none): A #GHashTable of in
 * parameter name and #GValue pairs
 * @out_hash: (inout) (element-type utf8 GValue) (transfer full): A #GHashTable
 * of out parameter name and initialized #GValue pairs
475
476
477
478
 *
 * See gupnp_service_proxy_send_action(); this version takes a pair of
 * #GHashTable<!-- -->s for runtime determined parameter lists.
 *
479
 * Return value: %TRUE if sending the action was succesful.
480
481
482
483
484
485
486
487
488
489
490
491
 **/
gboolean
gupnp_service_proxy_send_action_hash (GUPnPServiceProxy *proxy,
                                      const char        *action,
                                      GError           **error,
                                      GHashTable        *in_hash,
                                      GHashTable        *out_hash)
{
        GUPnPContext *context;
        GMainContext *main_context;
        GMainLoop *main_loop;
        GUPnPServiceProxyAction *handle;
492

493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
        g_return_val_if_fail (action, FALSE);

        context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
        main_context = gssdp_client_get_main_context (GSSDP_CLIENT (context));
        main_loop = g_main_loop_new (main_context, TRUE);

        handle = gupnp_service_proxy_begin_action_hash (proxy,
                                                        action,
                                                        stop_main_loop,
                                                        main_loop,
                                                        in_hash);
        if (!handle) {
                g_main_loop_unref (main_loop);

                return FALSE;
        }

        /* Loop till we get a reply (or time out) */
        if (g_main_loop_is_running (main_loop))
                g_main_loop_run (main_loop);

        g_main_loop_unref (main_loop);

        if (!gupnp_service_proxy_end_action_hash (proxy,
                                                  handle,
                                                  error,
                                                  out_hash))
                return FALSE;

        return TRUE;
}

526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
/**
 * gupnp_service_proxy_send_action_list:
 * @proxy: (transfer none) : A #GUPnPServiceProxy
 * @action: An action
 * @error: The location where to store any error, or %NULL
 * @in_names: (element-type utf8) (transfer none): #GList of 'in' parameter
 * names (as strings)
 * @in_values: (element-type GValue) (transfer none): #GList of values (as
 * #GValue) that line up with @in_names
 * @out_names: (element-type utf8) (transfer none): #GList of 'out' parameter
 * names (as strings)
 * @out_types: (element-type GType) (transfer none): #GList of types (as #GType)
 * that line up with @out_names
 * @out_values: (element-type GValue) (transfer full) (out): #GList of values
 * (as #GValue) that line up with @out_names and @out_types.
 *
 * The synchronous variant of #gupnp_service_proxy_begin_action_list and
 * #gupnp_service_proxy_end_action_list.
 *
 * Return value: %TRUE if sending the action was succesful.
 **/
gboolean
gupnp_service_proxy_send_action_list (GUPnPServiceProxy *proxy,
                                      const char        *action,
                                      GError           **error,
                                      GList             *in_names,
                                      GList             *in_values,
                                      GList             *out_names,
                                      GList             *out_types,
                                      GList            **out_values)
{
        GUPnPContext *context;
        GMainContext *main_context;
        GMainLoop *main_loop;
        GUPnPServiceProxyAction *handle;

        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
        g_return_val_if_fail (action, FALSE);

        context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
        main_context = gssdp_client_get_main_context (GSSDP_CLIENT (context));
        main_loop = g_main_loop_new (main_context, TRUE);

        handle = gupnp_service_proxy_begin_action_list (proxy,
                                                        action,
                                                        in_names,
                                                        in_values,
                                                        stop_main_loop,
                                                        main_loop);
        if (!handle) {
                g_main_loop_unref (main_loop);

                return FALSE;
        }

        /* Loop till we get a reply (or time out) */
        if (g_main_loop_is_running (main_loop))
                g_main_loop_run (main_loop);

        g_main_loop_unref (main_loop);

        if (!gupnp_service_proxy_end_action_list (proxy,
                                                  handle,
                                                  error,
                                                  out_names,
                                                  out_types,
                                                  out_values))
                return FALSE;

        return TRUE;
}

Jorn Baayen's avatar
Jorn Baayen committed
598
/**
599
 * gupnp_service_proxy_begin_action:
Jorn Baayen's avatar
Jorn Baayen committed
600
601
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
602
 * @callback: (scope async): The callback to call when sending the action has succeeded
Jorn Baayen's avatar
Jorn Baayen committed
603
604
 * or failed
 * @user_data: User data for @callback
605
 * @Varargs: tuples of in parameter name, in parameter type, and in parameter
606
 * value, terminated with %NULL
Jorn Baayen's avatar
Jorn Baayen committed
607
608
 *
 * Sends action @action with parameters @Varargs to the service exposed by
609
610
611
 * @proxy asynchronously, calling @callback on completion. From @callback, call
 * gupnp_service_proxy_end_action() to check for errors, to retrieve return
 * values, and to free the #GUPnPServiceProxyAction.
Jorn Baayen's avatar
Jorn Baayen committed
612
 *
613
 * Returns: (transfer none): A #GUPnPServiceProxyAction handle. This will be freed when
614
 * gupnp_service_proxy_cancel_action() or
615
 * gupnp_service_proxy_end_action_valist().
Jorn Baayen's avatar
Jorn Baayen committed
616
617
618
619
620
621
622
623
 **/
GUPnPServiceProxyAction *
gupnp_service_proxy_begin_action (GUPnPServiceProxy              *proxy,
                                  const char                     *action,
                                  GUPnPServiceProxyActionCallback callback,
                                  gpointer                        user_data,
                                  ...)
{
Jorn Baayen's avatar
Jorn Baayen committed
624
625
626
        va_list var_args;
        GUPnPServiceProxyAction *ret;

627
        va_start (var_args, user_data);
Jorn Baayen's avatar
Jorn Baayen committed
628
629
630
631
632
633
634
635
        ret = gupnp_service_proxy_begin_action_valist (proxy,
                                                       action,
                                                       callback,
                                                       user_data,
                                                       var_args);
        va_end (var_args);

        return ret;
Jorn Baayen's avatar
Jorn Baayen committed
636
637
}

638
/* Begins a basic action message */
639
640
641
642
643
static GUPnPServiceProxyAction *
begin_action_msg (GUPnPServiceProxy              *proxy,
                  const char                     *action,
                  GUPnPServiceProxyActionCallback callback,
                  gpointer                        user_data)
644
{
645
        GUPnPServiceProxyAction *ret;
646
        char *control_url, *full_action;
647
648
        const char *service_type;

649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
        /* Create action structure */
        ret = g_slice_new (GUPnPServiceProxyAction);

        ret->proxy = proxy;

        ret->callback  = callback;
        ret->user_data = user_data;

        ret->msg = NULL;

        ret->error = NULL;

        proxy->priv->pending_actions =
                g_list_prepend (proxy->priv->pending_actions, ret);

        /* Make sure we have a service type */
        service_type = gupnp_service_info_get_service_type
                                        (GUPNP_SERVICE_INFO (proxy));
        if (service_type == NULL) {
                ret->error = g_error_new (GUPNP_SERVER_ERROR,
                                          GUPNP_SERVER_ERROR_OTHER,
                                          "No service type defined");

                return ret;
        }

675
676
677
678
679
        /* Create message */
        control_url = gupnp_service_info_get_control_url
                                        (GUPNP_SERVICE_INFO (proxy));

        if (control_url != NULL) {
680
                ret->msg = soup_message_new (SOUP_METHOD_POST, control_url);
681
682
683
684

                g_free (control_url);
        }

685
686
687
688
        if (ret->msg == NULL) {
                ret->error = g_error_new (GUPNP_SERVER_ERROR,
                                          GUPNP_SERVER_ERROR_INVALID_URL,
                                          "No valid control URL defined");
689

690
                return ret;
691
692
693
694
        }

        /* Specify action */
        full_action = g_strdup_printf ("\"%s#%s\"", service_type, action);
695
        soup_message_headers_append (ret->msg->request_headers,
696
                                     "SOAPAction",
697
                                     full_action);
698
699
        g_free (full_action);

700
        /* Specify language */
701
        http_request_set_accept_language (ret->msg);
702

703
704
705
706
        /* Accept gzip encoding */
        soup_message_headers_append (ret->msg->request_headers,
				     "Accept-Encoding", "gzip");

707
        /* Set up envelope */
708
        ret->msg_str = xml_util_new_string ();
709

710
        g_string_append (ret->msg_str,
711
712
713
714
715
716
                         "<?xml version=\"1.0\"?>"
                         "<s:Envelope xmlns:s="
                                "\"http://schemas.xmlsoap.org/soap/envelope/\" "
                          "s:encodingStyle="
                                "\"http://schemas.xmlsoap.org/soap/encoding/\">"
                         "<s:Body>");
717

718
719
720
721
722
        g_string_append (ret->msg_str, "<u:");
        g_string_append (ret->msg_str, action);
        g_string_append (ret->msg_str, " xmlns:u=\"");
        g_string_append (ret->msg_str, service_type);
        g_string_append (ret->msg_str, "\">");
723

724
        return ret;
725
726
}

727
/* Received response to action message */
728
static void
729
730
action_got_response (SoupSession             *session,
                     SoupMessage             *msg,
731
                     GUPnPServiceProxyAction *action)
732
{
733
        const char *full_action;
734

735
736
737
738
739
740
        switch (msg->status_code) {
        case SOUP_STATUS_CANCELLED:
                /* Silently return */
                break;

        case SOUP_STATUS_METHOD_NOT_ALLOWED:
741
742
743
                /* Retry with M-POST */
                msg->method = "M-POST";

744
                soup_message_headers_append
745
746
747
748
749
                        (msg->request_headers,
                         "Man",
                         "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=s");

                /* Rename "SOAPAction" to "s-SOAPAction" */
750
751
752
                full_action = soup_message_headers_get_one
                        (msg->request_headers,
                         "SOAPAction");
753
754
755
756
                soup_message_headers_append (msg->request_headers,
                                             "s-SOAPAction",
                                             full_action);
                soup_message_headers_remove (msg->request_headers,
757
758
759
760
                                            "SOAPAction");

                /* And re-queue */
                soup_session_requeue_message (session, msg);
761
762
763
764

                break;

        default:
765
766
                /* Success: Call callback */
                action->callback (action->proxy, action, action->user_data);
767
768

                break;
769
        }
770
771
}

772
/* Finishes an action message and sends it off */
773
774
775
static void
finish_action_msg (GUPnPServiceProxyAction *action,
                   const char              *action_name)
776
777
778
779
780
{
        GUPnPContext *context;
        SoupSession *session;

        /* Finish message */
781
782
783
        g_string_append (action->msg_str, "</u:");
        g_string_append (action->msg_str, action_name);
        g_string_append_c (action->msg_str, '>');
784

785
        g_string_append (action->msg_str,
786
787
788
                         "</s:Body>"
                         "</s:Envelope>");

789
        soup_message_set_request (action->msg,
790
                                  "text/xml; charset=\"utf-8\"",
791
792
793
794
795
                                  SOUP_MEMORY_TAKE,
                                  action->msg_str->str,
                                  action->msg_str->len);

        g_string_free (action->msg_str, FALSE);
796

797
        /* We need to keep our own reference to the message as well,
798
         * in order for send_action() to work. */
799
        g_object_ref (action->msg);
800
801

        /* Send the message */
802
803
        context = gupnp_service_info_get_context
                                (GUPNP_SERVICE_INFO (action->proxy));
804
        session = gupnp_context_get_session (context);
805
806

        soup_session_queue_message (session,
807
                                    action->msg,
808
                                    (SoupSessionCallback) action_got_response,
809
                                    action);
810
811
812
813
}

/* Writes a parameter name and GValue pair to @msg */
static void
814
815
816
write_in_parameter (const char *arg_name,
                    GValue     *value,
                    GString    *msg_str)
817
{
818
        /* Write parameter pair */
819
820
821
        xml_util_start_element (msg_str, arg_name);
        gvalue_util_value_append_to_xml_string (value, msg_str);
        xml_util_end_element (msg_str, arg_name);
822
823
}

Jorn Baayen's avatar
Jorn Baayen committed
824
/**
825
 * gupnp_service_proxy_begin_action_valist:
Jorn Baayen's avatar
Jorn Baayen committed
826
827
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
828
 * @callback: (scope async) : The callback to call when sending the action has succeeded
Jorn Baayen's avatar
Jorn Baayen committed
829
830
 * or failed
 * @user_data: User data for @callback
831
 * @var_args: A va_list of tuples of in parameter name, in parameter type, and
Jorn Baayen's avatar
Jorn Baayen committed
832
833
834
835
836
 * in parameter value
 *
 * See gupnp_service_proxy_begin_action(); this version takes a va_list for
 * use by language bindings.
 *
837
 * Returns: (transfer none): A #GUPnPServiceProxyAction handle. This will
838
839
 * be freed when calling gupnp_service_proxy_cancel_action() or
 * gupnp_service_proxy_end_action_valist().
Jorn Baayen's avatar
Jorn Baayen committed
840
841
842
843
844
845
846
847
848
 **/
GUPnPServiceProxyAction *
gupnp_service_proxy_begin_action_valist
                                   (GUPnPServiceProxy              *proxy,
                                    const char                     *action,
                                    GUPnPServiceProxyActionCallback callback,
                                    gpointer                        user_data,
                                    va_list                         var_args)
{
849
        const char *arg_name;
850
        GUPnPServiceProxyAction *ret;
851

Jorn Baayen's avatar
Jorn Baayen committed
852
853
854
855
        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
        g_return_val_if_fail (action, NULL);
        g_return_val_if_fail (callback, NULL);

Jorn Baayen's avatar
Jorn Baayen committed
856
        /* Create message */
857
858
859
860
861
862
863
        ret = begin_action_msg (proxy, action, callback, user_data);

        if (ret->error) {
                callback (proxy, ret, user_data);

                return ret;
        }
864

Jorn Baayen's avatar
Jorn Baayen committed
865
        /* Arguments */
866
867
868
        arg_name = va_arg (var_args, const char *);
        while (arg_name) {
                GType arg_type;
869
                GValue value = { 0, };
870
                char *collect_error = NULL;
871
872
873
874

                arg_type = va_arg (var_args, GType);
                g_value_init (&value, arg_type);

875
876
                G_VALUE_COLLECT (&value, var_args, G_VALUE_NOCOPY_CONTENTS,
                                 &collect_error);
877
                if (!collect_error) {
878
                        write_in_parameter (arg_name, &value, ret->msg_str);
879
880
881
882

                        g_value_unset (&value);

                } else {
883
884
                        g_warning ("Error collecting value: %s\n",
                                   collect_error);
885
886

                        g_free (collect_error);
887

888
                        /* we purposely leak the value here, it might not be
889
890
                         * in a sane state if an error condition occoured
                         */
891
892
893
894
895
                }

                arg_name = va_arg (var_args, const char *);
        }

896
        /* Finish and send off */
897
        finish_action_msg (ret, action);
898

899
900
        /* Save the current position in the va_list for send_action_valist() */
        G_VA_COPY (ret->var_args, var_args);
901

902
903
        return ret;
}
904

905
906
907
908
909
910
911
912
/**
 * gupnp_service_proxy_begin_action_list:
 * @proxy: (transfer none): A #GUPnPServiceProxy
 * @action: An action
 * @in_names: (element-type utf8) (transfer none): #GList of 'in' parameter
 * names (as strings)
 * @in_values: (element-type GValue) (transfer none): #GList of values (as
 * #GValue) that line up with @in_names
913
 * @callback: (scope async) : The callback to call when sending the action has succeeded or
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
 * failed
 * @user_data: User data for @callback
 *
 * A variant of #gupnp_service_proxy_begin_action that takes lists of
 * in-parameter names, types and values.
 *
 * Return value: (transfer none): A #GUPnPServiceProxyAction handle. This will
 * be freed when calling #gupnp_service_proxy_cancel_action or
 * #gupnp_service_proxy_end_action_list.
 **/
GUPnPServiceProxyAction *
gupnp_service_proxy_begin_action_list
                                   (GUPnPServiceProxy               *proxy,
                                    const char                      *action,
                                    GList                           *in_names,
                                    GList                           *in_values,
                                    GUPnPServiceProxyActionCallback  callback,
                                    gpointer                         user_data)
{
        GUPnPServiceProxyAction *ret;
        GList *names;
        GList *values;

        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
        g_return_val_if_fail (action, NULL);
        g_return_val_if_fail (callback, NULL);
        g_return_val_if_fail (g_list_length (in_names) ==
                              g_list_length (in_values),
                              NULL);

        /* Create message */
        ret = begin_action_msg (proxy, action, callback, user_data);

        if (ret->error) {
                callback (proxy, ret, user_data);

                return ret;
        }

        /* Arguments */
        values = in_values;

        for (names = in_names; names; names=names->next) {
                GValue* val = values->data;

                write_in_parameter (names->data,
                                    val,
                                    ret->msg_str);

                values = values->next;
        }

        /* Finish and send off */
        finish_action_msg (ret, action);

        return ret;
}

972
/**
973
 * gupnp_service_proxy_begin_action_hash:
974
975
 * @proxy: A #GUPnPServiceProxy
 * @action: An action
976
 * @callback: (scope async): The callback to call when sending the action has succeeded
977
978
979
980
981
982
983
 * or failed
 * @user_data: User data for @callback
 * @hash: A #GHashTable of in parameter name and #GValue pairs
 *
 * See gupnp_service_proxy_begin_action(); this version takes a #GHashTable
 * for runtime generated parameter lists.
 *
984
 * Return value: (transfer none): A #GUPnPServiceProxyAction handle. This will
985
986
 * be freed when calling gupnp_service_proxy_cancel_action() or
 * gupnp_service_proxy_end_action_hash().
987
988
989
990
991
992
993
994
995
 **/
GUPnPServiceProxyAction *
gupnp_service_proxy_begin_action_hash
                                   (GUPnPServiceProxy              *proxy,
                                    const char                     *action,
                                    GUPnPServiceProxyActionCallback callback,
                                    gpointer                        user_data,
                                    GHashTable                     *hash)
{
996
        GUPnPServiceProxyAction *ret;
997

998
999
1000
        g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
        g_return_val_if_fail (action, NULL);
        g_return_val_if_fail (callback, NULL);
1001

1002
        /* Create message */
1003
1004
1005
1006
1007
1008
1009
        ret = begin_action_msg (proxy, action, callback, user_data);

        if (ret->error) {
                callback (proxy, ret, user_data);

                return ret;
        }
Jorn Baayen's avatar
Jorn Baayen committed
1010

1011
        /* Arguments */
1012
        g_hash_table_foreach (hash, (GHFunc) write_in_parameter, ret->msg_str);
1013

1014
        /* Finish and send off */
1015
1016
1017
        finish_action_msg (ret, action);

        return ret;
Jorn Baayen's avatar
Jorn Baayen committed
1018
1019
1020
}

/**
1021
 * gupnp_service_proxy_end_action:
Jorn Baayen's avatar
Jorn Baayen committed
1022
1023
 * @proxy: A #GUPnPServiceProxy
 * @action: A #GUPnPServiceProxyAction handle
1024
 * @error: The location where to store any error, or %NULL
1025
 * @Varargs: tuples of out parameter name, out parameter type, and out parameter
1026
 * value location, terminated with %NULL. The out parameter values should be
1027
 * freed after use
Jorn Baayen's avatar
Jorn Baayen committed
1028
 *
1029
 * Retrieves the result of @action. The out parameters in @Varargs will be
1030
1031
 * filled in, and if an error occurred, @error will be set. In case of
 * a UPnPError the error code will be the same in @error.
Jorn Baayen's avatar
Jorn Baayen committed
1032
 *
1033
 * Return value: %TRUE on success.
Jorn Baayen's avatar
Jorn Baayen committed
1034
1035
1036
1037
1038
1039
1040
 **/
gboolean
gupnp_service_proxy_end_action (GUPnPServiceProxy       *proxy,
                                GUPnPServiceProxyAction *action,
                                GError                 **error,
                                ...)
{
Jorn Baayen's avatar
Jorn Baayen committed
1041
1042
        va_list var_args;
        gboolean ret;
Jorn Baayen's avatar
Jorn Baayen committed
1043

Jorn Baayen's avatar
Jorn Baayen committed
1044
1045
1046
1047
1048
1049
1050
1051
        va_start (var_args, error);
        ret = gupnp_service_proxy_end_action_valist (proxy,
                                                     action,
                                                     error,
                                                     var_args);
        va_end (var_args);

        return ret;
Jorn Baayen's avatar
Jorn Baayen committed
1052
1053
}

1054
/* Checks an action response for errors and returns the parsed
1055
1056
 * xmlDoc object. */
static xmlDoc *
1057
1058
check_action_response (GUPnPServiceProxy       *proxy,
                       GUPnPServiceProxyAction *action,
1059
                       xmlNode                **params,
1060
                       GError                 **error)
Jorn Baayen's avatar
Jorn Baayen committed
1061
{
1062
        xmlDoc *response;
1063
        int code;
1064

1065
        /* Check for errors */
1066
        switch (action->msg->status_code) {
1067
1068
1069
1070
        case SOUP_STATUS_OK:
        case SOUP_STATUS_INTERNAL_SERVER_ERROR:
                break;
        default:
1071
                _gupnp_error_set_server_error (error, action->msg);
1072

1073
                return NULL;
1074
        }
1075
1076

        /* Parse response */
1077
1078
        response = xmlRecoverMemory (action->msg->response_body->data,
                                     action->msg->response_body->length);
1079

1080
        if (!response) {
1081
                if (action->msg->status_code == SOUP_STATUS_OK) {
1082
                        g_set_error (error,
1083
1084
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
1085
1086
                                     "Could not parse SOAP response");
                } else {
1087
                        g_set_error_literal
1088
1089
1090
                                    (error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INTERNAL_SERVER_ERROR,
1091
                                     action->msg->reason_phrase);
1092
                }
1093

1094
                return NULL;
1095
        }
1096

1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
        /* Get parameter list */
        *params = xml_util_get_element ((xmlNode *) response,
                                        "Envelope",
                                        NULL);
        if (*params != NULL)
                *params = xml_util_real_node ((*params)->children);

        if (*params != NULL) {
                if (strcmp ((const char *) (*params)->name, "Header") == 0)
                        *params = xml_util_real_node ((*params)->next);

                if (*params != NULL)
                        if (strcmp ((const char *) (*params)->name, "Body") != 0)
                                *params = NULL;
        }

        if (*params != NULL)
                *params = xml_util_real_node ((*params)->children);

        if (*params == NULL) {
                g_set_error (error,
                             GUPNP_SERVER_ERROR,
                             GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                             "Invalid Envelope");

                xmlFreeDoc (response);

                return NULL;
        }

1127
        /* Check whether we have a Fault */
1128
        if (action->msg->status_code == SOUP_STATUS_INTERNAL_SERVER_ERROR) {
1129
                xmlNode *param;
1130
1131
                char *desc;

1132
1133
1134
1135
                param = xml_util_get_element (*params,
                                              "detail",
                                              "UPnPError",
                                              NULL);
1136

1137
1138
1139
1140
1141
                if (!param) {
                        g_set_error (error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                     "Invalid Fault");
1142

1143
                        xmlFreeDoc (response);
1144

1145
                        return NULL;
1146
                }
1147

1148
                /* Code */
1149
1150
1151
                code = xml_util_get_child_element_content_int
                                        (param, "errorCode");
                if (code == -1) {
1152
1153
1154
1155
1156
                        g_set_error (error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                     "Invalid Fault");

1157
                        xmlFreeDoc (response);
1158

1159
                        return NULL;
1160
                }
1161
1162

                /* Description */
1163
1164
1165
                desc = xml_util_get_child_element_content_glib
                                        (param, "errorDescription");
                if (desc == NULL)
1166
                        desc = g_strdup (action->msg->reason_phrase);
1167

1168
1169
1170
1171
                g_set_error_literal (error,
                                     GUPNP_CONTROL_ERROR,
                                     code,
                                     desc);
1172
1173

                g_free (desc);
1174

1175
                xmlFreeDoc (response);
1176

1177
                return NULL;
1178
1179
        }

1180
1181
        return response;
}
1182

1183
/* Reads a value into the parameter name and initialised GValue pair
1184
1185
 * from @response */
static void
1186
1187
1188
read_out_parameter (const char *arg_name,
                    GValue     *value,
                    xmlNode    *params)
1189
{
1190
        xmlNode *param;
1191

1192
        /* Try to find a matching parameter in the response*/
1193
1194
1195
        param = xml_util_get_element (params,
                                      arg_name,
                                      NULL);
1196
1197
1198
1199
1200
1201
        if (!param) {
                g_warning ("Could not find variable \"%s\" in response",
                           arg_name);

                return;
        }
Jorn Baayen's avatar
Jorn Baayen committed
1202

1203
        gvalue_util_set_value_from_xml_node (value, param);
1204
}
1205

1206
/**