gupnp-context.c 32.9 KB
Newer Older
1
/*
2
 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3
 * Copyright (C) 2009 Nokia Corporation, all rights reserved.
Jorn Baayen's avatar
Jorn Baayen committed
4
5
 *
 * Author: Jorn Baayen <jorn@openedhand.com>
6
7
 *         Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 *                               <zeeshan.ali@nokia.com>
Jorn Baayen's avatar
Jorn Baayen committed
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 *
 * 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.
 */

25
26
27
28
29
30
/**
 * SECTION:gupnp-context
 * @short_description: Context object wrapping shared networking bits.
 *
 * #GUPnPContext wraps the networking bits that are used by the various
 * GUPnP classes. It automatically starts a web server on demand.
31
32
33
 *
 * For debugging, it is possible to see the messages being sent and received by
 * exporting #GUPNP_DEBUG.
34
35
 */

36
#include <config.h>
37
38
39
#include <errno.h>
#include <string.h>
#include <unistd.h>
40
#include <fcntl.h>
41
42
#include <stdlib.h>
#include <stdio.h>
43
#include <sys/utsname.h>
44
#include <sys/ioctl.h>
45
46
#include <sys/socket.h>
#include <sys/types.h>
47
#include <sys/stat.h>
48
#include <libsoup/soup-address.h>
49
#include <glib/gstdio.h>
50

Jorn Baayen's avatar
Jorn Baayen committed
51
#include "gupnp-context.h"
52
#include "gupnp-context-private.h"
53
#include "gupnp-marshal.h"
54
#include "gena-protocol.h"
55
#include "http-headers.h"
Jorn Baayen's avatar
Jorn Baayen committed
56

57
58
59
60
61
62
63
64
65
66
67
68
static void
gupnp_context_initable_iface_init (gpointer g_iface,
                                   gpointer iface_data);


G_DEFINE_TYPE_EXTENDED (GUPnPContext,
                        gupnp_context,
                        GSSDP_TYPE_CLIENT,
                        0,
                        G_IMPLEMENT_INTERFACE
                                (G_TYPE_INITABLE,
                                 gupnp_context_initable_iface_init));
Jorn Baayen's avatar
Jorn Baayen committed
69
70

struct _GUPnPContextPrivate {
71
72
        guint        port;

73
74
        guint        subscription_timeout;

75
        SoupSession *session;
76
77
78

        SoupServer  *server; /* Started on demand */
        char        *server_url;
79
80

        GList       *host_path_datas;
81
82
83
84
};

enum {
        PROP_0,
85
        PROP_PORT,
86
        PROP_SERVER,
87
        PROP_SESSION,
88
        PROP_SUBSCRIPTION_TIMEOUT
Jorn Baayen's avatar
Jorn Baayen committed
89
90
};

91
92
93
94
95
96
typedef struct {
        char *local_path;

        GRegex *regex;
} UserAgent;

97
98
99
typedef struct {
        char *local_path;
        char *server_path;
100
101

        GList *user_agents;
102
} HostPathData;
103

104
105
static GInitableIface* initable_parent_iface = NULL;

106
/*
107
 * Generates the default server ID.
108
109
110
111
112
113
114
 **/
static char *
make_server_id (void)
{
        struct utsname sysinfo;

        uname (&sysinfo);
115

116
117
        return g_strdup_printf ("%s/%s UPnP/1.0 GUPnP/%s",
                                sysinfo.sysname,
118
                                sysinfo.release,
119
120
121
                                VERSION);
}

Jorn Baayen's avatar
Jorn Baayen committed
122
123
124
static void
gupnp_context_init (GUPnPContext *context)
{
125
126
        char *server_id;

Jorn Baayen's avatar
Jorn Baayen committed
127
128
129
130
        context->priv =
                G_TYPE_INSTANCE_GET_PRIVATE (context,
                                             GUPNP_TYPE_CONTEXT,
                                             GUPnPContextPrivate);
131

132
133
134
        server_id = make_server_id ();
        gssdp_client_set_server_id (GSSDP_CLIENT (context), server_id);
        g_free (server_id);
135
136
}

137
138
139
140
static gboolean
gupnp_context_initable_init (GInitable     *initable,
                             GCancellable  *cancellable,
                             GError       **error)
141
{
142
        char *user_agent;
143
144
        GError *inner_error = NULL;
        GUPnPContext *context;
145

146
147
148
149
150
151
152
153
154
        if (!initable_parent_iface->init(initable,
                                         cancellable,
                                         &inner_error)) {
                g_propagate_error (error, inner_error);

                return FALSE;
        }

        context = GUPNP_CONTEXT (initable);
155
156
157
158
159
160
161

        context->priv->session = soup_session_async_new_with_options
                (SOUP_SESSION_IDLE_TIMEOUT,
                 60,
                 SOUP_SESSION_ASYNC_CONTEXT,
                 gssdp_client_get_main_context (GSSDP_CLIENT (context)),
                 NULL);
162

163
164
165
166
167
168
169
170
        user_agent = g_strdup_printf ("%s GUPnP/" VERSION " DLNADOC/1.50",
                                      g_get_application_name ()? : "");
        g_object_set (context->priv->session,
                      SOUP_SESSION_USER_AGENT,
                      user_agent,
                      NULL);
        g_free (user_agent);

171
172
173
        if (g_getenv ("GUPNP_DEBUG")) {
                SoupLogger *logger;
                logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
174
175
                soup_session_add_feature (context->priv->session,
                                          SOUP_SESSION_FEATURE (logger));
176
        }
177

178
179
180
        soup_session_add_feature_by_type (context->priv->session,
                                          SOUP_TYPE_CONTENT_DECODER);

181
182
183
184
185
186
187
188
189
190
        return TRUE;
}

static void
gupnp_context_initable_iface_init (gpointer g_iface,
                                   gpointer iface_data)
{
        GInitableIface *iface = (GInitableIface *)g_iface;
        initable_parent_iface = g_type_interface_peek_parent (iface);
        iface->init = gupnp_context_initable_init;
Jorn Baayen's avatar
Jorn Baayen committed
191
192
}

193
194
195
196
197
198
199
200
201
202
203
204
205
206
static void
gupnp_context_set_property (GObject      *object,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
        GUPnPContext *context;

        context = GUPNP_CONTEXT (object);

        switch (property_id) {
        case PROP_PORT:
                context->priv->port = g_value_get_uint (value);
                break;
207
208
209
        case PROP_SUBSCRIPTION_TIMEOUT:
                context->priv->subscription_timeout = g_value_get_uint (value);
                break;
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

static void
gupnp_context_get_property (GObject    *object,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
        GUPnPContext *context;

        context = GUPNP_CONTEXT (object);

        switch (property_id) {
        case PROP_PORT:
228
229
230
                g_value_set_uint (value,
                                  gupnp_context_get_port (context));
                break;
231
232
233
234
        case PROP_SERVER:
                g_value_set_object (value,
                                    gupnp_context_get_server (context));
                break;
235
236
237
238
        case PROP_SESSION:
                g_value_set_object (value,
                                    gupnp_context_get_session (context));
                break;
239
240
241
242
        case PROP_SUBSCRIPTION_TIMEOUT:
                g_value_set_uint (value,
                                  gupnp_context_get_subscription_timeout
                                                                   (context));
243
244
245
246
247
248
249
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

Jorn Baayen's avatar
Jorn Baayen committed
250
251
252
253
static void
gupnp_context_dispose (GObject *object)
{
        GUPnPContext *context;
254
        GObjectClass *object_class;
Jorn Baayen's avatar
Jorn Baayen committed
255
256
257

        context = GUPNP_CONTEXT (object);

258
259
260
261
        if (context->priv->session) {
                g_object_unref (context->priv->session);
                context->priv->session = NULL;
        }
262

263
264
265
266
267
268
269
270
        while (context->priv->host_path_datas) {
                HostPathData *data;

                data = (HostPathData *) context->priv->host_path_datas->data;

                gupnp_context_unhost_path (context, data->server_path);
        }

271
272
273
274
275
        if (context->priv->server) {
                g_object_unref (context->priv->server);
                context->priv->server = NULL;
        }

276
277
278
        /* Call super */
        object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
        object_class->dispose (object);
279
280
281
282
283
284
}

static void
gupnp_context_finalize (GObject *object)
{
        GUPnPContext *context;
285
        GObjectClass *object_class;
286
287
288
289

        context = GUPNP_CONTEXT (object);

        g_free (context->priv->server_url);
290
291
292
293

        /* Call super */
        object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
        object_class->finalize (object);
Jorn Baayen's avatar
Jorn Baayen committed
294
295
296
297
298
299
300
301
302
}

static void
gupnp_context_class_init (GUPnPContextClass *klass)
{
        GObjectClass *object_class;

        object_class = G_OBJECT_CLASS (klass);

303
304
305
306
        object_class->set_property = gupnp_context_set_property;
        object_class->get_property = gupnp_context_get_property;
        object_class->dispose      = gupnp_context_dispose;
        object_class->finalize     = gupnp_context_finalize;
Jorn Baayen's avatar
Jorn Baayen committed
307
308

        g_type_class_add_private (klass, sizeof (GUPnPContextPrivate));
309

310
        /**
311
         * GUPnPContext:port:
312
313
314
         *
         * The port to run on. Set to 0 if you don't care what port to run on.
         **/
315
316
317
318
319
320
        g_object_class_install_property
                (object_class,
                 PROP_PORT,
                 g_param_spec_uint ("port",
                                    "Port",
                                    "Port to run on",
321
322
                                    0, G_MAXUINT, SOUP_ADDRESS_ANY_PORT,
                                    G_PARAM_READWRITE |
323
324
325
326
                                    G_PARAM_CONSTRUCT_ONLY |
                                    G_PARAM_STATIC_NAME |
                                    G_PARAM_STATIC_NICK |
                                    G_PARAM_STATIC_BLURB));
327

328
        /**
329
         * GUPnPContext:server:
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
         *
         * The #SoupServer HTTP server used by GUPnP.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_SERVER,
                 g_param_spec_object ("server",
                                      "SoupServer",
                                      "SoupServer HTTP server",
                                      SOUP_TYPE_SERVER,
                                      G_PARAM_READABLE |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

345
        /**
346
         * GUPnPContext:session:
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
         *
         * The #SoupSession object used by GUPnP.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_SESSION,
                 g_param_spec_object ("session",
                                      "SoupSession",
                                      "SoupSession object",
                                      SOUP_TYPE_SESSION,
                                      G_PARAM_READABLE |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

362
        /**
363
         * GUPnPContext:subscription-timeout:
364
365
         *
         * The preferred subscription timeout: the number of seconds after
366
367
         * which subscriptions are renewed. Set to '0' if subscriptions 
         * are never to time out.
368
         **/
369
370
371
372
373
374
        g_object_class_install_property
                (object_class,
                 PROP_SUBSCRIPTION_TIMEOUT,
                 g_param_spec_uint ("subscription-timeout",
                                    "Subscription timeout",
                                    "Subscription timeout",
375
376
                                    0,
                                    GENA_MAX_TIMEOUT,
377
                                    GENA_DEFAULT_TIMEOUT,
378
                                    G_PARAM_READWRITE |
379
380
381
382
                                    G_PARAM_CONSTRUCT_ONLY |
                                    G_PARAM_STATIC_NAME |
                                    G_PARAM_STATIC_NICK |
                                    G_PARAM_STATIC_BLURB));
Jorn Baayen's avatar
Jorn Baayen committed
383
384
}

385
/**
386
 * gupnp_context_get_session:
387
388
389
390
 * @context: A #GUPnPContext
 *
 * Get the #SoupSession object that GUPnP is using.
 *
391
392
 * Return value: (transfer none): The #SoupSession used by GUPnP. Do not unref
 * this when finished.
393
 **/
394
SoupSession *
395
gupnp_context_get_session (GUPnPContext *context)
396
{
397
398
        g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);

399
400
401
        return context->priv->session;
}

402
/*
403
 * Default server handler: Return 404 not found.
404
 **/
405
static void
406
407
408
409
410
default_server_handler (SoupServer        *server,
                        SoupMessage       *msg, 
                        const char        *path,
                        GHashTable        *query,
                        SoupClientContext *client,
411
                        gpointer           user_data)
412
{
413
        soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
414
415
}

416
/**
417
 * gupnp_context_get_server:
418
419
 * @context: A #GUPnPContext
 *
420
421
 * Get the #SoupServer HTTP server that GUPnP is using.
 *
422
 * Returns: (transfer none): The #SoupServer used by GUPnP. Do not unref this when finished.
423
 **/
424
SoupServer *
425
gupnp_context_get_server (GUPnPContext *context)
426
{
427
428
        g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);

429
        if (context->priv->server == NULL) {
Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
430
431
432
                const char *ip;

                ip =  gssdp_client_get_host_ip (GSSDP_CLIENT (context));
Ross Burton's avatar
Ross Burton committed
433
                SoupAddress *addr = soup_address_new (ip, context->priv->port);
434
435
                soup_address_resolve_sync (addr, NULL);

436
437
438
439
440
                context->priv->server = soup_server_new
                        (SOUP_SERVER_PORT,
                         context->priv->port,
                         SOUP_SERVER_ASYNC_CONTEXT,
                         gssdp_client_get_main_context (GSSDP_CLIENT (context)),
441
442
                         SOUP_SERVER_INTERFACE,
                         addr,
443
                         NULL);
444
                g_object_unref (addr);
445

446
447
448
                soup_server_add_handler (context->priv->server, NULL,
                                         default_server_handler, context,
                                         NULL);
449

450
451
                soup_server_run_async (context->priv->server);
        }
452

453
454
455
        return context->priv->server;
}

456
/*
457
458
459
460
461
462
463
464
465
 * Makes an URL that refers to our server.
 **/
static char *
make_server_url (GUPnPContext *context)
{
        SoupServer *server;
        guint port;

        /* What port are we running on? */
466
        server = gupnp_context_get_server (context);
467
468
469
        port = soup_server_get_port (server);

        /* Put it all together */
470
471
472
473
        return g_strdup_printf
                        ("http://%s:%u",
                         gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
                         port);
474
475
476
477
478
479
480
481
482
483
484
}

const char *
_gupnp_context_get_server_url (GUPnPContext *context)
{
        if (context->priv->server_url == NULL)
                context->priv->server_url = make_server_url (context);

        return (const char *) context->priv->server_url;
}

Jorn Baayen's avatar
Jorn Baayen committed
485
/**
486
 * gupnp_context_new:
487
 * @main_context: A #GMainContext, or %NULL to use the default one
488
 * @interface: The network interface to use, or %NULL to auto-detect.
489
 * @port: Port to run on, or 0 if you don't care what port is used.
490
491
 * @error: A location to store a #GError, or %NULL
 *
492
493
 * Create a new #GUPnPContext with the specified @main_context, @interface and
 * @port.
Jorn Baayen's avatar
Jorn Baayen committed
494
 *
495
 * Return value: A new #GUPnPContext object, or %NULL on an error
Jorn Baayen's avatar
Jorn Baayen committed
496
497
498
 **/
GUPnPContext *
gupnp_context_new (GMainContext *main_context,
499
                   const char   *interface,
500
                   guint         port,
Jorn Baayen's avatar
Jorn Baayen committed
501
502
                   GError      **error)
{
503
504
505
506
507
508
509
        return g_initable_new (GUPNP_TYPE_CONTEXT,
                               NULL,
                               error,
                               "main-context", main_context,
                               "interface", interface,
                               "port", port,
                               NULL);
Jorn Baayen's avatar
Jorn Baayen committed
510
}
511
512

/**
513
 * gupnp_context_get_host_ip:
514
515
 * @context: A #GUPnPContext
 *
516
517
518
 * Get the IP address we advertise ourselves as using.
 *
 * Return value: The IP address. This string should not be freed.
519
520
521
522
 *
 * Deprecated:0.12.7: The "host-ip" property has moved to the base class
 * #GSSDPClient so newer applications should use
 * #gssdp_client_get_host_ip instead.
523
524
525
526
 **/
const char *
gupnp_context_get_host_ip (GUPnPContext *context)
{
527
        return gssdp_client_get_host_ip (GSSDP_CLIENT (context));
528
529
530
}

/**
531
 * gupnp_context_get_port:
532
533
 * @context: A #GUPnPContext
 *
534
535
 * Get the port that the SOAP server is running on.
 
536
537
538
539
540
541
542
543
544
 * Return value: The port the SOAP server is running on.
 **/
guint
gupnp_context_get_port (GUPnPContext *context)
{
        SoupServer *server;

        g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);

545
        server = gupnp_context_get_server (context);
546
547
548
549
        return soup_server_get_port (server);
}

/**
550
 * gupnp_context_set_subscription_timeout:
551
552
553
 * @context: A #GUPnPContext
 * @timeout: Event subscription timeout in seconds
 *
554
555
556
 * Sets the event subscription timeout to @timeout. Use 0 if you don't
 * want subscriptions to time out. Note that any client side subscriptions
 * will automatically be renewed.
557
558
559
560
561
562
563
564
565
566
567
568
569
 **/
void
gupnp_context_set_subscription_timeout (GUPnPContext *context,
                                        guint         timeout)
{
        g_return_if_fail (GUPNP_IS_CONTEXT (context));

        context->priv->subscription_timeout = timeout;

        g_object_notify (G_OBJECT (context), "subscription-timeout");
}

/**
570
 * gupnp_context_get_subscription_timeout:
571
572
 * @context: A #GUPnPContext
 *
573
574
 * Get the event subscription timeout (in seconds), or 0 meaning there is no
 * timeout.
575
576
 * 
 * Return value: The event subscription timeout in seconds.
577
578
579
580
581
582
583
584
 **/
guint
gupnp_context_get_subscription_timeout (GUPnPContext *context)
{
        g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);

        return context->priv->subscription_timeout;
}
585

586
587
588
589
/* Construct a local path from @requested path, removing the last slash
 * if any to make sure we append the locale suffix in a canonical way. */
static char *
construct_local_path (const char   *requested_path,
590
                      const char   *user_agent,
591
592
593
                      HostPathData *host_path_data)
{
        GString *str;
594
        char *local_path;
595
596
        int len;

597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
        local_path = NULL;

        if (user_agent != NULL) {
                GList *node;

                for (node = host_path_data->user_agents;
                     node;
                     node = node->next) {
                        UserAgent *agent;

                        agent = node->data;

                        if (g_regex_match (agent->regex,
                                           user_agent,
                                           0,
                                           NULL)) {
                                local_path = agent->local_path;
                        }
                }
        }

        if (local_path == NULL)
                local_path = host_path_data->local_path;

621
        if (!requested_path || *requested_path == 0)
622
                return g_strdup (local_path);
623
624
625
626

        if (*requested_path != '/')
                return NULL; /* Absolute paths only */

627
        str = g_string_new (local_path);
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
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

        /* Skip the length of the path relative to which @requested_path
         * is specified. */
        requested_path += strlen (host_path_data->server_path);

        /* Strip the last slashes to make sure we append the locale suffix
         * in a canonical way. */
        len = strlen (requested_path);
        while (requested_path[len - 1] == '/')
                len--;

        g_string_append_len (str,
                             requested_path,
                             len);

        return g_string_free (str, FALSE);
}

/* Append locale suffix to @local_path. */
static char *
append_locale (const char *local_path, GList *locales)
{
        if (!locales)
                return g_strdup (local_path);

        return g_strdup_printf ("%s.%s",
                                local_path,
                                (char *) locales->data);
}

/* Redirect @msg to the same URI, but with a slash appended. */
static void
redirect_to_folder (SoupMessage *msg)
{
        char *uri, *redir_uri;

        uri = soup_uri_to_string (soup_message_get_uri (msg),
                                  FALSE);
        redir_uri = g_strdup_printf ("%s/", uri);
        soup_message_headers_append (msg->response_headers,
                                     "Location", redir_uri);
        soup_message_set_status (msg,
                                 SOUP_STATUS_MOVED_PERMANENTLY);
        g_free (redir_uri);
        g_free (uri);
}

675
676
/* Serve @path. Note that we do not need to check for path including bogus
 * '..' as libsoup does this for us. */
677
static void
678
679
680
681
682
683
host_path_handler (SoupServer        *server,
                   SoupMessage       *msg, 
                   const char        *path,
                   GHashTable        *query,
                   SoupClientContext *client,
                   gpointer           user_data)
684
{
685
        char *local_path, *path_to_open;
686
        struct stat st;
687
        int status;
688
        GList *locales, *orig_locales;
689
690
        GMappedFile *mapped_file;
        GError *error;
691
692
        HostPathData *host_path_data;
        const char *user_agent;
693

694
        orig_locales = NULL;
695
696
697
        locales      = NULL;
        local_path   = NULL;
        path_to_open = NULL;
698
        host_path_data = (HostPathData *) user_data;
699

700
701
        if (msg->method != SOUP_METHOD_GET &&
            msg->method != SOUP_METHOD_HEAD) {
702
                soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
703

704
705
706
                goto DONE;
        }

707
708
709
        user_agent = soup_message_headers_get_one (msg->request_headers,
                                                   "User-Agent");

710
        /* Construct base local path */
711
        local_path = construct_local_path (path, user_agent, host_path_data);
712
713
        if (!local_path) {
                soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
714

715
                goto DONE;
716
717
        }

718
        /* Get preferred locales */
719
        orig_locales = locales = http_request_get_accept_locales (msg);
720

721
722
 AGAIN:
        /* Add locale suffix if available */
723
        path_to_open = append_locale (local_path, locales);
724

725
        /* See what we've got */
726
        if (g_stat (path_to_open, &st) == -1) {
727
728
                if (errno == EPERM)
                        soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
729
730
                else if (errno == ENOENT) {
                        if (locales) {
731
732
                                g_free (path_to_open);

733
                                locales = locales->next;
734

735
                                goto AGAIN;
736
737
738
739
                        } else
                                soup_message_set_status (msg,
                                                         SOUP_STATUS_NOT_FOUND);
                } else
740
741
                        soup_message_set_status
                                (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
742

743
744
745
                goto DONE;
        }

746
        /* Handle directories */
747
        if (S_ISDIR (st.st_mode)) {
748
749
                if (!g_str_has_suffix (path, "/")) {
                        redirect_to_folder (msg);
750

751
752
753
                        goto DONE;
                }

754
755
756
                /* This incorporates the locale portion in the folder name
                 * intentionally. */
                g_free (local_path);
757
758
759
                local_path = g_build_filename (path_to_open,
                                               "index.html",
                                               NULL);
760

761
                g_free (path_to_open);
762

763
764
765
                goto AGAIN;
        }

766
767
768
769
770
771
772
773
774
775
        /* Map file */
        error = NULL;
        mapped_file = g_mapped_file_new (path_to_open, FALSE, &error);

        if (mapped_file == NULL) {
                g_warning ("Unable to map file %s: %s",
                           path_to_open, error->message);

                g_error_free (error);

776
777
                soup_message_set_status (msg,
                                         SOUP_STATUS_INTERNAL_SERVER_ERROR);
778

779
780
781
                goto DONE;
        }

782
783
784
        /* Handle method (GET or HEAD) */
        status = SOUP_STATUS_OK;

785
        if (msg->method == SOUP_METHOD_GET) {
786
                gsize offset, length;
787
788
                gboolean have_range;
                SoupBuffer *buffer;
789

790
791
                /* Find out range */
                have_range = FALSE;
792

793
794
795
                offset = 0;
                length = st.st_size;

796
797
798
799
                if (!http_request_get_range (msg,
                                             &have_range,
                                             &offset,
                                             &length)) {
800
801
802
803
804
805
806
                        soup_message_set_status
                                (msg,
                                 SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);

                        goto DONE;
                }

Philip Withnall's avatar
Philip Withnall committed
807
808
809
                if (have_range && (length > st.st_size - offset ||
                                   st.st_size < 0 ||
                                   (off_t) offset >= st.st_size)) {
810
811
812
813
814
815
816
817
818
819
820
821
                        soup_message_set_status
                                (msg,
                                 SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);

                        goto DONE;
                }

                /* Add requested content */
                buffer = soup_buffer_new_with_owner
                             (g_mapped_file_get_contents (mapped_file) + offset,
                              length,
                              mapped_file,
822
                              (GDestroyNotify) g_mapped_file_unref);
823
824
825
826
827
828
829

                soup_message_body_append_buffer (msg->response_body, buffer);

                soup_buffer_free (buffer);

                /* Set status */
                if (have_range) {
830
831
832
833
                        http_response_set_content_range (msg,
                                                         offset,
                                                         offset + length,
                                                         st.st_size);
834

835
836
                        status = SOUP_STATUS_PARTIAL_CONTENT;
                }
837

838
839
        } else if (msg->method == SOUP_METHOD_HEAD) {
                char *length;
840

841
                length = g_strdup_printf ("%lu", (gulong) st.st_size);
842
                soup_message_headers_append (msg->response_headers,
843
                                             "Content-Length",
844
                                             length);
845
                g_free (length);
846

847
        } else {
Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
848
849
                soup_message_set_status (msg,
                                         SOUP_STATUS_METHOD_NOT_ALLOWED);
850

Zeeshan Ali (Khattak)'s avatar
Zeeshan Ali (Khattak) committed
851
                goto DONE;
852
        }
853

854
        /* Set Content-Type */
855
856
857
858
859
        http_response_set_content_type (msg,
                                        path_to_open, 
                                        (guchar *) g_mapped_file_get_contents
                                                                (mapped_file),
                                        st.st_size);
860

861
        /* Set Content-Language */
862
        if (locales)
863
               http_response_set_content_locale (msg, locales->data);
864

865
866
867
868
        /* Set Accept-Ranges */
        soup_message_headers_append (msg->response_headers,
                                     "Accept-Ranges",
                                     "bytes");
869

870
871
872
        /* Set status */
        soup_message_set_status (msg, status);

873
 DONE:
874
        /* Cleanup */
875
876
877
        g_free (path_to_open);
        g_free (local_path);

878
879
880
        while (orig_locales) {
                g_free (orig_locales->data);
                orig_locales = g_list_delete_link (orig_locales, orig_locales);
881
        }
882
883
}

884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
static UserAgent *
user_agent_new (const char *local_path,
                GRegex     *regex)
{
        UserAgent *agent;

        agent = g_slice_new0 (UserAgent);

        agent->local_path = g_strdup (local_path);
        agent->regex = g_regex_ref (regex);

        return agent;
}

static void
user_agent_free (UserAgent *agent)
{
        g_free (agent->local_path);
        g_regex_unref (agent->regex);

        g_slice_free (UserAgent, agent);
}

Philip Withnall's avatar
Philip Withnall committed
907
static HostPathData *
908
909
host_path_data_new (const char *local_path,
                    const char *server_path)
910
{
911
        HostPathData *path_data;
912

913
        path_data = g_slice_new0 (HostPathData);
914
915
916
917
918
919
920

        path_data->local_path  = g_strdup (local_path);
        path_data->server_path = g_strdup (server_path);

        return path_data;
}

Philip Withnall's avatar
Philip Withnall committed
921
static void
922
host_path_data_free (HostPathData *path_data)
923
924
925
{
        g_free (path_data->local_path);
        g_free (path_data->server_path);
926
927
928
929
930
931
932
933
934
935
936
        while (path_data->user_agents) {
                UserAgent *agent;

                agent = path_data->user_agents->data;

                user_agent_free (agent);

                path_data->user_agents = g_list_delete_link (
                                path_data->user_agents,
                                path_data->user_agents);
        }
937

938
        g_slice_free (HostPathData, path_data);
939
940
}

941
/**
942
 * gupnp_context_host_path:
943
944
945
946
 * @context: A #GUPnPContext
 * @local_path: Path to the local file or folder to be hosted
 * @server_path: Web server path where @local_path should be hosted
 *
947
948
 * Start hosting @local_path at @server_path. Files with the path
 * @local_path.LOCALE (if they exist) will be served up when LOCALE is
949
 * specified in the request's Accept-Language header.
950
951
952
953
954
955
956
 **/
void
gupnp_context_host_path (GUPnPContext *context,
                         const char   *local_path,
                         const char   *server_path)
{
        SoupServer *server;
957
        HostPathData *path_data;
958
959
960
961
962

        g_return_if_fail (GUPNP_IS_CONTEXT (context));
        g_return_if_fail (local_path != NULL);
        g_return_if_fail (server_path != NULL);

963
        server = gupnp_context_get_server (context);
964

965
966
        path_data = host_path_data_new (local_path,
                                        server_path);
967
968
969

        soup_server_add_handler (server,
                                 server_path,
970
                                 host_path_handler,
971
                                 path_data,
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
                                 NULL);

        context->priv->host_path_datas =
                g_list_append (context->priv->host_path_datas,
                               path_data);
}

static unsigned int
path_compare_func (HostPathData *path_data,
                   const char   *server_path)
{
        return strcmp (path_data->server_path, server_path);
}

/**
987
 * gupnp_context_host_path_for_agent:
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
 * @context: A #GUPnPContext
 * @local_path: Path to the local file or folder to be hosted
 * @server_path: Web server path already being hosted
 * @user_agent: The user-agent as a #GRegex.
 *
 * Use this method to serve different local path to specific user-agent(s). The
 * path @server_path must already be hosted by @context.
 *
 * Return value: %TRUE on success, %FALSE otherwise.
 **/
gboolean
gupnp_context_host_path_for_agent (GUPnPContext *context,
                                   const char   *local_path,
                                   const char   *server_path,
                                   GRegex       *user_agent)
{
        GList *node;

        g_return_val_if_fail (GUPNP_IS_CONTEXT (context), FALSE);
        g_return_val_if_fail (local_path != NULL, FALSE);
        g_return_val_if_fail (server_path != NULL, FALSE);
        g_return_val_if_fail (user_agent != NULL, FALSE);

        node = g_list_find_custom (context->priv->host_path_datas,
                                   server_path,
                                   (GCompareFunc) path_compare_func);
        if (node != NULL) {
                HostPathData *path_data;
                UserAgent *agent;

                path_data = (HostPathData *) node->data;
                agent = user_agent_new (local_path, user_agent);

                path_data->user_agents = g_list_append (path_data->user_agents,
                                                        agent);

                return TRUE;
        } else
                return FALSE;
1027
1028
1029
}

/**
1030
 * gupnp_context_unhost_path:
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
 * @context: A #GUPnPContext
 * @server_path: Web server path where the file or folder is hosted
 *
 * Stop hosting the file or folder at @server_path.
 **/
void
gupnp_context_unhost_path (GUPnPContext *context,
                           const char   *server_path)
{
        SoupServer *server;
1041
1042
        HostPathData *path_data;
        GList *node;
1043
1044
1045
1046

        g_return_if_fail (GUPNP_IS_CONTEXT (context));
        g_return_if_fail (server_path != NULL);

1047
        server = gupnp_context_get_server (context);
1048

1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
        node = g_list_find_custom (context->priv->host_path_datas,
                                   server_path,
                                   (GCompareFunc) path_compare_func);
        g_return_if_fail (node != NULL);

        path_data = (HostPathData *) node->data;
        context->priv->host_path_datas = g_list_delete_link (
                        context->priv->host_path_datas,
                        node);

1059
        soup_server_remove_handler (server, server_path);
1060
        host_path_data_free (path_data);
1061
}