114b1b3344d7299d418e00fe3642dd45c4371323
[profile/ivi/GUPnP.git] / libgupnp / gupnp-context.c
1 /*
2  * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3  * Copyright (C) 2009 Nokia Corporation.
4  *
5  * Author: Jorn Baayen <jorn@openedhand.com>
6  *         Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
7  *                               <zeeshan.ali@nokia.com>
8  *
9  * This library is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Library General Public
11  * License as published by the Free Software Foundation; either
12  * version 2 of the License, or (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Library General Public License for more details.
18  *
19  * You should have received a copy of the GNU Library General Public
20  * License along with this library; if not, write to the
21  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  * Boston, MA 02110-1301, USA.
23  */
24
25 /**
26  * SECTION:gupnp-context
27  * @short_description: Context object wrapping shared networking bits.
28  *
29  * #GUPnPContext wraps the networking bits that are used by the various
30  * GUPnP classes. It automatically starts a web server on demand.
31  *
32  * For debugging, it is possible to see the messages being sent and received by
33  * exporting #GUPNP_DEBUG.
34  */
35
36 #include <config.h>
37 #include <errno.h>
38 #include <string.h>
39 #include <unistd.h>
40 #include <fcntl.h>
41 #include <stdlib.h>
42 #include <stdio.h>
43 #include <sys/utsname.h>
44 #include <sys/ioctl.h>
45 #include <sys/socket.h>
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 #include <libsoup/soup-address.h>
49 #include <glib/gstdio.h>
50
51 #include "gupnp-context.h"
52 #include "gupnp-context-private.h"
53 #include "gupnp-error.h"
54 #include "gupnp-marshal.h"
55 #include "gena-protocol.h"
56 #include "http-headers.h"
57
58 #define GUPNP_CONTEXT_DEFAULT_LANGUAGE "en"
59
60 static void
61 gupnp_context_initable_iface_init (gpointer g_iface,
62                                    gpointer iface_data);
63
64
65 G_DEFINE_TYPE_EXTENDED (GUPnPContext,
66                         gupnp_context,
67                         GSSDP_TYPE_CLIENT,
68                         0,
69                         G_IMPLEMENT_INTERFACE
70                                 (G_TYPE_INITABLE,
71                                  gupnp_context_initable_iface_init));
72
73 struct _GUPnPContextPrivate {
74         guint        port;
75
76         guint        subscription_timeout;
77
78         SoupSession *session;
79
80         SoupServer  *server; /* Started on demand */
81         char        *server_url;
82         char        *default_language;
83
84         GList       *host_path_datas;
85 };
86
87 enum {
88         PROP_0,
89         PROP_PORT,
90         PROP_SERVER,
91         PROP_SESSION,
92         PROP_SUBSCRIPTION_TIMEOUT,
93         PROP_DEFAULT_LANGUAGE
94 };
95
96 typedef struct {
97         char *local_path;
98
99         GRegex *regex;
100 } UserAgent;
101
102 typedef struct {
103         char *local_path;
104         char *server_path;
105         char *default_language;
106
107         GList *user_agents;
108 } HostPathData;
109
110 static GInitableIface* initable_parent_iface = NULL;
111
112 /*
113  * Generates the default server ID.
114  **/
115 static char *
116 make_server_id (void)
117 {
118         struct utsname sysinfo;
119
120         uname (&sysinfo);
121
122         return g_strdup_printf ("%s/%s UPnP/1.0 GUPnP/%s",
123                                 sysinfo.sysname,
124                                 sysinfo.release,
125                                 VERSION);
126 }
127
128 static void
129 gupnp_context_init (GUPnPContext *context)
130 {
131         char *server_id;
132
133         context->priv =
134                 G_TYPE_INSTANCE_GET_PRIVATE (context,
135                                              GUPNP_TYPE_CONTEXT,
136                                              GUPnPContextPrivate);
137
138         server_id = make_server_id ();
139         gssdp_client_set_server_id (GSSDP_CLIENT (context), server_id);
140         g_free (server_id);
141 }
142
143 static gboolean
144 gupnp_context_initable_init (GInitable     *initable,
145                              GCancellable  *cancellable,
146                              GError       **error)
147 {
148         char *user_agent;
149         GError *inner_error = NULL;
150         GUPnPContext *context;
151
152         if (!initable_parent_iface->init(initable,
153                                          cancellable,
154                                          &inner_error)) {
155                 g_propagate_error (error, inner_error);
156
157                 return FALSE;
158         }
159
160         context = GUPNP_CONTEXT (initable);
161
162         context->priv->session = soup_session_async_new_with_options
163                 (SOUP_SESSION_IDLE_TIMEOUT,
164                  60,
165                  SOUP_SESSION_ASYNC_CONTEXT,
166                  g_main_context_get_thread_default (),
167                  NULL);
168
169         user_agent = g_strdup_printf ("%s GUPnP/" VERSION " DLNADOC/1.50",
170                                       g_get_application_name ()? : "");
171         g_object_set (context->priv->session,
172                       SOUP_SESSION_USER_AGENT,
173                       user_agent,
174                       NULL);
175         g_free (user_agent);
176
177         if (g_getenv ("GUPNP_DEBUG")) {
178                 SoupLogger *logger;
179                 logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
180                 soup_session_add_feature (context->priv->session,
181                                           SOUP_SESSION_FEATURE (logger));
182         }
183
184         soup_session_add_feature_by_type (context->priv->session,
185                                           SOUP_TYPE_CONTENT_DECODER);
186
187         /* Create the server already if the port is not null*/
188         if (context->priv->port != 0) {
189                 gupnp_context_get_server (context);
190
191                 if (context->priv->server == NULL) {
192                         g_object_unref (context->priv->session);
193                         context->priv->session = NULL;
194
195                         g_set_error (error,
196                                      GUPNP_SERVER_ERROR,
197                                      GUPNP_SERVER_ERROR_OTHER,
198                                      "Could not create HTTP server on port %d",
199                                      context->priv->port);
200
201                         return FALSE;
202                 }
203         }
204
205         return TRUE;
206 }
207
208 static void
209 gupnp_context_initable_iface_init (gpointer g_iface,
210                                    gpointer iface_data)
211 {
212         GInitableIface *iface = (GInitableIface *)g_iface;
213         initable_parent_iface = g_type_interface_peek_parent (iface);
214         iface->init = gupnp_context_initable_init;
215 }
216
217 static void
218 gupnp_context_set_property (GObject      *object,
219                             guint         property_id,
220                             const GValue *value,
221                             GParamSpec   *pspec)
222 {
223         GUPnPContext *context;
224
225         context = GUPNP_CONTEXT (object);
226
227         switch (property_id) {
228         case PROP_PORT:
229                 context->priv->port = g_value_get_uint (value);
230                 break;
231         case PROP_SUBSCRIPTION_TIMEOUT:
232                 context->priv->subscription_timeout = g_value_get_uint (value);
233                 break;
234         case PROP_DEFAULT_LANGUAGE:
235                 gupnp_context_set_default_language (context,
236                                                     g_value_get_string (value));
237                 break;
238         default:
239                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
240                 break;
241         }
242 }
243
244 static void
245 gupnp_context_get_property (GObject    *object,
246                             guint       property_id,
247                             GValue     *value,
248                             GParamSpec *pspec)
249 {
250         GUPnPContext *context;
251
252         context = GUPNP_CONTEXT (object);
253
254         switch (property_id) {
255         case PROP_PORT:
256                 g_value_set_uint (value,
257                                   gupnp_context_get_port (context));
258                 break;
259         case PROP_SERVER:
260                 g_value_set_object (value,
261                                     gupnp_context_get_server (context));
262                 break;
263         case PROP_SESSION:
264                 g_value_set_object (value,
265                                     gupnp_context_get_session (context));
266                 break;
267         case PROP_SUBSCRIPTION_TIMEOUT:
268                 g_value_set_uint (value,
269                                   gupnp_context_get_subscription_timeout
270                                                                    (context));
271                 break;
272         case PROP_DEFAULT_LANGUAGE:
273                 g_value_set_string (value,
274                                     gupnp_context_get_default_language
275                                                                    (context));
276                 break;
277         default:
278                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
279                 break;
280         }
281 }
282
283 static void
284 gupnp_context_dispose (GObject *object)
285 {
286         GUPnPContext *context;
287         GObjectClass *object_class;
288
289         context = GUPNP_CONTEXT (object);
290
291         if (context->priv->session) {
292                 g_object_unref (context->priv->session);
293                 context->priv->session = NULL;
294         }
295
296         while (context->priv->host_path_datas) {
297                 HostPathData *data;
298
299                 data = (HostPathData *) context->priv->host_path_datas->data;
300
301                 gupnp_context_unhost_path (context, data->server_path);
302         }
303
304         if (context->priv->server) {
305                 g_object_unref (context->priv->server);
306                 context->priv->server = NULL;
307         }
308
309         /* Call super */
310         object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
311         object_class->dispose (object);
312 }
313
314 static void
315 gupnp_context_finalize (GObject *object)
316 {
317         GUPnPContext *context;
318         GObjectClass *object_class;
319
320         context = GUPNP_CONTEXT (object);
321
322         if (context->priv->default_language) {
323                 g_free (context->priv->default_language);
324                 context->priv->default_language = NULL;
325         }
326
327         g_free (context->priv->server_url);
328
329         /* Call super */
330         object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
331         object_class->finalize (object);
332 }
333
334 static void
335 gupnp_context_class_init (GUPnPContextClass *klass)
336 {
337         GObjectClass *object_class;
338
339         object_class = G_OBJECT_CLASS (klass);
340
341         object_class->set_property = gupnp_context_set_property;
342         object_class->get_property = gupnp_context_get_property;
343         object_class->dispose      = gupnp_context_dispose;
344         object_class->finalize     = gupnp_context_finalize;
345
346         g_type_class_add_private (klass, sizeof (GUPnPContextPrivate));
347
348         /**
349          * GUPnPContext:port:
350          *
351          * The port to run on. Set to 0 if you don't care what port to run on.
352          **/
353         g_object_class_install_property
354                 (object_class,
355                  PROP_PORT,
356                  g_param_spec_uint ("port",
357                                     "Port",
358                                     "Port to run on",
359                                     0, G_MAXUINT, SOUP_ADDRESS_ANY_PORT,
360                                     G_PARAM_READWRITE |
361                                     G_PARAM_CONSTRUCT_ONLY |
362                                     G_PARAM_STATIC_NAME |
363                                     G_PARAM_STATIC_NICK |
364                                     G_PARAM_STATIC_BLURB));
365
366         /**
367          * GUPnPContext:server:
368          *
369          * The #SoupServer HTTP server used by GUPnP.
370          **/
371         g_object_class_install_property
372                 (object_class,
373                  PROP_SERVER,
374                  g_param_spec_object ("server",
375                                       "SoupServer",
376                                       "SoupServer HTTP server",
377                                       SOUP_TYPE_SERVER,
378                                       G_PARAM_READABLE |
379                                       G_PARAM_STATIC_NAME |
380                                       G_PARAM_STATIC_NICK |
381                                       G_PARAM_STATIC_BLURB));
382
383         /**
384          * GUPnPContext:session:
385          *
386          * The #SoupSession object used by GUPnP.
387          **/
388         g_object_class_install_property
389                 (object_class,
390                  PROP_SESSION,
391                  g_param_spec_object ("session",
392                                       "SoupSession",
393                                       "SoupSession object",
394                                       SOUP_TYPE_SESSION,
395                                       G_PARAM_READABLE |
396                                       G_PARAM_STATIC_NAME |
397                                       G_PARAM_STATIC_NICK |
398                                       G_PARAM_STATIC_BLURB));
399
400         /**
401          * GUPnPContext:subscription-timeout:
402          *
403          * The preferred subscription timeout: the number of seconds after
404          * which subscriptions are renewed. Set to '0' if subscriptions 
405          * are never to time out.
406          **/
407         g_object_class_install_property
408                 (object_class,
409                  PROP_SUBSCRIPTION_TIMEOUT,
410                  g_param_spec_uint ("subscription-timeout",
411                                     "Subscription timeout",
412                                     "Subscription timeout",
413                                     0,
414                                     GENA_MAX_TIMEOUT,
415                                     GENA_DEFAULT_TIMEOUT,
416                                     G_PARAM_READWRITE |
417                                     G_PARAM_CONSTRUCT_ONLY |
418                                     G_PARAM_STATIC_NAME |
419                                     G_PARAM_STATIC_NICK |
420                                     G_PARAM_STATIC_BLURB));
421         /**
422          * GUPnPContext:default-language:
423          *
424          * The content of the Content-Language header id the client
425          * sends Accept-Language and no language-specific pages to serve
426          * exist. The property defaults to 'en'.
427          **/
428         g_object_class_install_property
429                 (object_class,
430                  PROP_DEFAULT_LANGUAGE,
431                  g_param_spec_string ("default-language",
432                                       "Default language",
433                                       "Default language",
434                                       GUPNP_CONTEXT_DEFAULT_LANGUAGE,
435                                       G_PARAM_READWRITE |
436                                       G_PARAM_CONSTRUCT |
437                                       G_PARAM_STATIC_NAME |
438                                       G_PARAM_STATIC_NICK |
439                                       G_PARAM_STATIC_BLURB));
440 }
441
442 /**
443  * gupnp_context_get_session:
444  * @context: A #GUPnPContext
445  *
446  * Get the #SoupSession object that GUPnP is using.
447  *
448  * Return value: (transfer none): The #SoupSession used by GUPnP. Do not unref
449  * this when finished.
450  **/
451 SoupSession *
452 gupnp_context_get_session (GUPnPContext *context)
453 {
454         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
455
456         return context->priv->session;
457 }
458
459 /*
460  * Default server handler: Return 404 not found.
461  **/
462 static void
463 default_server_handler (SoupServer        *server,
464                         SoupMessage       *msg, 
465                         const char        *path,
466                         GHashTable        *query,
467                         SoupClientContext *client,
468                         gpointer           user_data)
469 {
470         soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
471 }
472
473 /**
474  * gupnp_context_get_server:
475  * @context: A #GUPnPContext
476  *
477  * Get the #SoupServer HTTP server that GUPnP is using.
478  *
479  * Returns: (transfer none): The #SoupServer used by GUPnP. Do not unref this when finished.
480  **/
481 SoupServer *
482 gupnp_context_get_server (GUPnPContext *context)
483 {
484         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
485
486         if (context->priv->server == NULL) {
487                 const char *ip;
488
489                 ip =  gssdp_client_get_host_ip (GSSDP_CLIENT (context));
490                 SoupAddress *addr = soup_address_new (ip, context->priv->port);
491                 soup_address_resolve_sync (addr, NULL);
492
493                 context->priv->server = soup_server_new
494                         (SOUP_SERVER_PORT,
495                          context->priv->port,
496                          SOUP_SERVER_ASYNC_CONTEXT,
497                          g_main_context_get_thread_default (),
498                          SOUP_SERVER_INTERFACE,
499                          addr,
500                          NULL);
501                 g_object_unref (addr);
502
503                 if (context->priv->server) {
504                         soup_server_add_handler (context->priv->server,
505                                                  NULL,
506                                                  default_server_handler,
507                                                  context,
508                                                  NULL);
509
510                         soup_server_run_async (context->priv->server);
511                 }
512         }
513
514         return context->priv->server;
515 }
516
517 /*
518  * Makes an URL that refers to our server.
519  **/
520 static char *
521 make_server_url (GUPnPContext *context)
522 {
523         SoupServer *server;
524         guint port;
525
526         /* What port are we running on? */
527         server = gupnp_context_get_server (context);
528         port = soup_server_get_port (server);
529
530         /* Put it all together */
531         return g_strdup_printf
532                         ("http://%s:%u",
533                          gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
534                          port);
535 }
536
537 const char *
538 _gupnp_context_get_server_url (GUPnPContext *context)
539 {
540         if (context->priv->server_url == NULL)
541                 context->priv->server_url = make_server_url (context);
542
543         return (const char *) context->priv->server_url;
544 }
545
546 /**
547  * gupnp_context_new:
548  * @main_context: (allow-none): Deprecated: 0.17.2: Always set to %NULL. If you
549  * want to use a different context, use g_main_context_push_thread_default().
550  * @interface: (allow-none): The network interface to use, or %NULL to
551  * auto-detect.
552  * @port: Port to run on, or 0 if you don't care what port is used.
553  * @error: A location to store a #GError, or %NULL
554  *
555  * Create a new #GUPnPContext with the specified @main_context, @interface and
556  * @port.
557  *
558  * Return value: A new #GUPnPContext object, or %NULL on an error
559  **/
560 GUPnPContext *
561 gupnp_context_new (GMainContext *main_context,
562                    const char   *interface,
563                    guint         port,
564                    GError      **error)
565 {
566         if (main_context)
567                 g_warning ("gupnp_context_new::main_context is deprecated."
568                            " Use g_main_context_push_thread_default()"
569                            " instead");
570
571         return g_initable_new (GUPNP_TYPE_CONTEXT,
572                                NULL,
573                                error,
574                                "interface", interface,
575                                "port", port,
576                                NULL);
577 }
578
579 /**
580  * gupnp_context_get_host_ip:
581  * @context: A #GUPnPContext
582  *
583  * Get the IP address we advertise ourselves as using.
584  *
585  * Return value: The IP address. This string should not be freed.
586  *
587  * Deprecated:0.12.7: The "host-ip" property has moved to the base class
588  * #GSSDPClient so newer applications should use
589  * #gssdp_client_get_host_ip instead.
590  **/
591 const char *
592 gupnp_context_get_host_ip (GUPnPContext *context)
593 {
594         return gssdp_client_get_host_ip (GSSDP_CLIENT (context));
595 }
596
597 /**
598  * gupnp_context_get_port:
599  * @context: A #GUPnPContext
600  *
601  * Get the port that the SOAP server is running on.
602  
603  * Return value: The port the SOAP server is running on.
604  **/
605 guint
606 gupnp_context_get_port (GUPnPContext *context)
607 {
608         SoupServer *server;
609
610         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);
611
612         server = gupnp_context_get_server (context);
613         return soup_server_get_port (server);
614 }
615
616 /**
617  * gupnp_context_set_subscription_timeout:
618  * @context: A #GUPnPContext
619  * @timeout: Event subscription timeout in seconds
620  *
621  * Sets the event subscription timeout to @timeout. Use 0 if you don't
622  * want subscriptions to time out. Note that any client side subscriptions
623  * will automatically be renewed.
624  **/
625 void
626 gupnp_context_set_subscription_timeout (GUPnPContext *context,
627                                         guint         timeout)
628 {
629         g_return_if_fail (GUPNP_IS_CONTEXT (context));
630
631         context->priv->subscription_timeout = timeout;
632
633         g_object_notify (G_OBJECT (context), "subscription-timeout");
634 }
635
636 /**
637  * gupnp_context_get_subscription_timeout:
638  * @context: A #GUPnPContext
639  *
640  * Get the event subscription timeout (in seconds), or 0 meaning there is no
641  * timeout.
642  * 
643  * Return value: The event subscription timeout in seconds.
644  **/
645 guint
646 gupnp_context_get_subscription_timeout (GUPnPContext *context)
647 {
648         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);
649
650         return context->priv->subscription_timeout;
651 }
652
653 static void
654 host_path_data_set_language (HostPathData *data, const char *language)
655 {
656         char *old_language = data->default_language;
657
658         if ((old_language != NULL) && (!strcmp (language, old_language)))
659                 return;
660
661         data->default_language = g_strdup (language);
662
663
664         if (old_language != NULL)
665                 g_free (old_language);
666 }
667
668 /**
669  * gupnp_context_set_default_language:
670  * @context: A #GUPnPContext
671  * @language A language tag as defined in RFC 2616 3.10
672  *
673  * Set the default language for the Content-Length header to @language.
674  *
675  * If the client sends an Accept-Language header the UPnP HTTP server
676  * is required to send a Content-Language header in return. If there are
677  * no files hosted in languages which match the requested ones the
678  * Content-Language header is set to this value. The default value is "en".
679  */
680 void
681 gupnp_context_set_default_language (GUPnPContext *context,
682                                     const char   *language)
683 {
684         g_return_if_fail (GUPNP_IS_CONTEXT (context));
685         g_return_if_fail (language != NULL);
686
687
688         char *old_language = context->priv->default_language;
689
690         if ((old_language != NULL) && (!strcmp (language, old_language)))
691                 return;
692
693         context->priv->default_language = g_strdup (language);
694
695         g_list_foreach (context->priv->host_path_datas,
696                         (GFunc) host_path_data_set_language,
697                         (gpointer) language);
698
699         if (old_language != NULL)
700                 g_free (old_language);
701 }
702
703 /**
704  * gupnp_context_get_default_language:
705  * @context: A #GUPnPContext
706  *
707  * Get the default Content-Language header for this context.
708  *
709  * Returns: (transfer none): The default content of the Content-Language
710  * header.
711  */
712 const char *
713 gupnp_context_get_default_language (GUPnPContext *context)
714 {
715         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
716
717         return context->priv->default_language;
718 }
719
720 /* Construct a local path from @requested path, removing the last slash
721  * if any to make sure we append the locale suffix in a canonical way. */
722 static char *
723 construct_local_path (const char   *requested_path,
724                       const char   *user_agent,
725                       HostPathData *host_path_data)
726 {
727         GString *str;
728         char *local_path;
729         int len;
730
731         local_path = NULL;
732
733         if (user_agent != NULL) {
734                 GList *node;
735
736                 for (node = host_path_data->user_agents;
737                      node;
738                      node = node->next) {
739                         UserAgent *agent;
740
741                         agent = node->data;
742
743                         if (g_regex_match (agent->regex,
744                                            user_agent,
745                                            0,
746                                            NULL)) {
747                                 local_path = agent->local_path;
748                         }
749                 }
750         }
751
752         if (local_path == NULL)
753                 local_path = host_path_data->local_path;
754
755         if (!requested_path || *requested_path == 0)
756                 return g_strdup (local_path);
757
758         if (*requested_path != '/')
759                 return NULL; /* Absolute paths only */
760
761         str = g_string_new (local_path);
762
763         /* Skip the length of the path relative to which @requested_path
764          * is specified. */
765         requested_path += strlen (host_path_data->server_path);
766
767         /* Strip the last slashes to make sure we append the locale suffix
768          * in a canonical way. */
769         len = strlen (requested_path);
770         while (requested_path[len - 1] == '/')
771                 len--;
772
773         g_string_append_len (str,
774                              requested_path,
775                              len);
776
777         return g_string_free (str, FALSE);
778 }
779
780 /* Append locale suffix to @local_path. */
781 static char *
782 append_locale (const char *local_path, GList *locales)
783 {
784         if (!locales)
785                 return g_strdup (local_path);
786
787         return g_strdup_printf ("%s.%s",
788                                 local_path,
789                                 (char *) locales->data);
790 }
791
792 /* Redirect @msg to the same URI, but with a slash appended. */
793 static void
794 redirect_to_folder (SoupMessage *msg)
795 {
796         char *uri, *redir_uri;
797
798         uri = soup_uri_to_string (soup_message_get_uri (msg),
799                                   FALSE);
800         redir_uri = g_strdup_printf ("%s/", uri);
801         soup_message_headers_append (msg->response_headers,
802                                      "Location", redir_uri);
803         soup_message_set_status (msg,
804                                  SOUP_STATUS_MOVED_PERMANENTLY);
805         g_free (redir_uri);
806         g_free (uri);
807 }
808
809 /* Serve @path. Note that we do not need to check for path including bogus
810  * '..' as libsoup does this for us. */
811 static void
812 host_path_handler (SoupServer        *server,
813                    SoupMessage       *msg, 
814                    const char        *path,
815                    GHashTable        *query,
816                    SoupClientContext *client,
817                    gpointer           user_data)
818 {
819         char *local_path, *path_to_open;
820         struct stat st;
821         int status;
822         GList *locales, *orig_locales;
823         GMappedFile *mapped_file;
824         GError *error;
825         HostPathData *host_path_data;
826         const char *user_agent;
827
828         orig_locales = NULL;
829         locales      = NULL;
830         local_path   = NULL;
831         path_to_open = NULL;
832         host_path_data = (HostPathData *) user_data;
833
834         if (msg->method != SOUP_METHOD_GET &&
835             msg->method != SOUP_METHOD_HEAD) {
836                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
837
838                 goto DONE;
839         }
840
841         user_agent = soup_message_headers_get_one (msg->request_headers,
842                                                    "User-Agent");
843
844         /* Construct base local path */
845         local_path = construct_local_path (path, user_agent, host_path_data);
846         if (!local_path) {
847                 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
848
849                 goto DONE;
850         }
851
852         /* Get preferred locales */
853         orig_locales = locales = http_request_get_accept_locales (msg);
854
855  AGAIN:
856         /* Add locale suffix if available */
857         path_to_open = append_locale (local_path, locales);
858
859         /* See what we've got */
860         if (g_stat (path_to_open, &st) == -1) {
861                 if (errno == EPERM)
862                         soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
863                 else if (errno == ENOENT) {
864                         if (locales) {
865                                 g_free (path_to_open);
866
867                                 locales = locales->next;
868
869                                 goto AGAIN;
870                         } else
871                                 soup_message_set_status (msg,
872                                                          SOUP_STATUS_NOT_FOUND);
873                 } else
874                         soup_message_set_status
875                                 (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
876
877                 goto DONE;
878         }
879
880         /* Handle directories */
881         if (S_ISDIR (st.st_mode)) {
882                 if (!g_str_has_suffix (path, "/")) {
883                         redirect_to_folder (msg);
884
885                         goto DONE;
886                 }
887
888                 /* This incorporates the locale portion in the folder name
889                  * intentionally. */
890                 g_free (local_path);
891                 local_path = g_build_filename (path_to_open,
892                                                "index.html",
893                                                NULL);
894
895                 g_free (path_to_open);
896
897                 goto AGAIN;
898         }
899
900         /* Map file */
901         error = NULL;
902         mapped_file = g_mapped_file_new (path_to_open, FALSE, &error);
903
904         if (mapped_file == NULL) {
905                 g_warning ("Unable to map file %s: %s",
906                            path_to_open, error->message);
907
908                 g_error_free (error);
909
910                 soup_message_set_status (msg,
911                                          SOUP_STATUS_INTERNAL_SERVER_ERROR);
912
913                 goto DONE;
914         }
915
916         /* Handle method (GET or HEAD) */
917         status = SOUP_STATUS_OK;
918
919         if (msg->method == SOUP_METHOD_GET) {
920                 gboolean have_range;
921                 SoupBuffer *buffer;
922                 SoupRange *ranges;
923                 int nranges;
924
925                 /* Find out range */
926                 have_range = FALSE;
927
928                 if (soup_message_headers_get_ranges (msg->request_headers,
929                                                      st.st_size,
930                                                      &ranges,
931                                                      &nranges))
932                         have_range = TRUE;
933
934                 /* We do not support mulipart/byteranges so only first first */
935                 /* range from request is handled */
936                 if (have_range && (ranges[0].end > st.st_size ||
937                                    st.st_size < 0 ||
938                                    ranges[0].start >= st.st_size ||
939                                    ranges[0].start > ranges[0].end)) {
940                         soup_message_set_status
941                                 (msg,
942                                  SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
943                         soup_message_headers_free_ranges (msg->request_headers,
944                                                           ranges);
945
946                         goto DONE;
947                 }
948
949                 /* Add requested content */
950                 buffer = soup_buffer_new_with_owner
951                              (g_mapped_file_get_contents (mapped_file),
952                               g_mapped_file_get_length (mapped_file),
953                               mapped_file,
954                               (GDestroyNotify) g_mapped_file_unref);
955
956                 /* Set range and status */
957                 if (have_range) {
958                         SoupBuffer *range_buffer;
959
960                         soup_message_body_truncate (msg->response_body);
961                         soup_message_headers_set_content_range (
962                                                           msg->response_headers,
963                                                           ranges[0].start,
964                                                           ranges[0].end,
965                                                           buffer->length);
966                         range_buffer = soup_buffer_new_subbuffer (
967                                            buffer,
968                                            ranges[0].start,
969                                            ranges[0].end - ranges[0].start + 1);
970                         soup_message_body_append_buffer (msg->response_body,
971                                                          range_buffer);
972                         status = SOUP_STATUS_PARTIAL_CONTENT;
973
974                         soup_message_headers_free_ranges (msg->request_headers,
975                                                           ranges);
976                         soup_buffer_free (range_buffer);
977                 } else
978                         soup_message_body_append_buffer (msg->response_body, buffer);
979
980                 soup_buffer_free (buffer);
981         } else if (msg->method == SOUP_METHOD_HEAD) {
982                 char *length;
983
984                 length = g_strdup_printf ("%lu", (gulong) st.st_size);
985                 soup_message_headers_append (msg->response_headers,
986                                              "Content-Length",
987                                              length);
988                 g_free (length);
989
990         } else {
991                 soup_message_set_status (msg,
992                                          SOUP_STATUS_METHOD_NOT_ALLOWED);
993
994                 goto DONE;
995         }
996
997         /* Set Content-Type */
998         http_response_set_content_type (msg,
999                                         path_to_open, 
1000                                         (guchar *) g_mapped_file_get_contents
1001                                                                 (mapped_file),
1002                                         st.st_size);
1003
1004         /* Set Content-Language */
1005         if (locales)
1006                 http_response_set_content_locale (msg, locales->data);
1007         else if (soup_message_headers_get_one (msg->request_headers,
1008                                                "Accept-Language")) {
1009                 soup_message_headers_append (msg->response_headers,
1010                                              "Content-Language",
1011                                              host_path_data->default_language);
1012         }
1013
1014         /* Set Accept-Ranges */
1015         soup_message_headers_append (msg->response_headers,
1016                                      "Accept-Ranges",
1017                                      "bytes");
1018
1019         /* Set status */
1020         soup_message_set_status (msg, status);
1021
1022  DONE:
1023         /* Cleanup */
1024         g_free (path_to_open);
1025         g_free (local_path);
1026
1027         while (orig_locales) {
1028                 g_free (orig_locales->data);
1029                 orig_locales = g_list_delete_link (orig_locales, orig_locales);
1030         }
1031 }
1032
1033 static UserAgent *
1034 user_agent_new (const char *local_path,
1035                 GRegex     *regex)
1036 {
1037         UserAgent *agent;
1038
1039         agent = g_slice_new0 (UserAgent);
1040
1041         agent->local_path = g_strdup (local_path);
1042         agent->regex = g_regex_ref (regex);
1043
1044         return agent;
1045 }
1046
1047 static void
1048 user_agent_free (UserAgent *agent)
1049 {
1050         g_free (agent->local_path);
1051         g_regex_unref (agent->regex);
1052
1053         g_slice_free (UserAgent, agent);
1054 }
1055
1056 static HostPathData *
1057 host_path_data_new (const char *local_path,
1058                     const char *server_path,
1059                     const char *default_language)
1060 {
1061         HostPathData *path_data;
1062
1063         path_data = g_slice_new0 (HostPathData);
1064
1065         path_data->local_path  = g_strdup (local_path);
1066         path_data->server_path = g_strdup (server_path);
1067         path_data->default_language = g_strdup (default_language);
1068
1069         return path_data;
1070 }
1071
1072 static void
1073 host_path_data_free (HostPathData *path_data)
1074 {
1075         g_free (path_data->local_path);
1076         g_free (path_data->server_path);
1077         g_free (path_data->default_language);
1078
1079         while (path_data->user_agents) {
1080                 UserAgent *agent;
1081
1082                 agent = path_data->user_agents->data;
1083
1084                 user_agent_free (agent);
1085
1086                 path_data->user_agents = g_list_delete_link (
1087                                 path_data->user_agents,
1088                                 path_data->user_agents);
1089         }
1090
1091         g_slice_free (HostPathData, path_data);
1092 }
1093
1094 /**
1095  * gupnp_context_host_path:
1096  * @context: A #GUPnPContext
1097  * @local_path: Path to the local file or folder to be hosted
1098  * @server_path: Web server path where @local_path should be hosted
1099  *
1100  * Start hosting @local_path at @server_path. Files with the path
1101  * @local_path.LOCALE (if they exist) will be served up when LOCALE is
1102  * specified in the request's Accept-Language header.
1103  **/
1104 void
1105 gupnp_context_host_path (GUPnPContext *context,
1106                          const char   *local_path,
1107                          const char   *server_path)
1108 {
1109         SoupServer *server;
1110         HostPathData *path_data;
1111
1112         g_return_if_fail (GUPNP_IS_CONTEXT (context));
1113         g_return_if_fail (local_path != NULL);
1114         g_return_if_fail (server_path != NULL);
1115
1116         server = gupnp_context_get_server (context);
1117
1118         path_data = host_path_data_new (local_path,
1119                                         server_path,
1120                                         context->priv->default_language);
1121
1122         soup_server_add_handler (server,
1123                                  server_path,
1124                                  host_path_handler,
1125                                  path_data,
1126                                  NULL);
1127
1128         context->priv->host_path_datas =
1129                 g_list_append (context->priv->host_path_datas,
1130                                path_data);
1131 }
1132
1133 static unsigned int
1134 path_compare_func (HostPathData *path_data,
1135                    const char   *server_path)
1136 {
1137         /* ignore default language */
1138         return strcmp (path_data->server_path, server_path);
1139 }
1140
1141 /**
1142  * gupnp_context_host_path_for_agent:
1143  * @context: A #GUPnPContext
1144  * @local_path: Path to the local file or folder to be hosted
1145  * @server_path: Web server path already being hosted
1146  * @user_agent: The user-agent as a #GRegex.
1147  *
1148  * Use this method to serve different local path to specific user-agent(s). The
1149  * path @server_path must already be hosted by @context.
1150  *
1151  * Return value: %TRUE on success, %FALSE otherwise.
1152  **/
1153 gboolean
1154 gupnp_context_host_path_for_agent (GUPnPContext *context,
1155                                    const char   *local_path,
1156                                    const char   *server_path,
1157                                    GRegex       *user_agent)
1158 {
1159         GList *node;
1160
1161         g_return_val_if_fail (GUPNP_IS_CONTEXT (context), FALSE);
1162         g_return_val_if_fail (local_path != NULL, FALSE);
1163         g_return_val_if_fail (server_path != NULL, FALSE);
1164         g_return_val_if_fail (user_agent != NULL, FALSE);
1165
1166         node = g_list_find_custom (context->priv->host_path_datas,
1167                                    server_path,
1168                                    (GCompareFunc) path_compare_func);
1169         if (node != NULL) {
1170                 HostPathData *path_data;
1171                 UserAgent *agent;
1172
1173                 path_data = (HostPathData *) node->data;
1174                 agent = user_agent_new (local_path, user_agent);
1175
1176                 path_data->user_agents = g_list_append (path_data->user_agents,
1177                                                         agent);
1178
1179                 return TRUE;
1180         } else
1181                 return FALSE;
1182 }
1183
1184 /**
1185  * gupnp_context_unhost_path:
1186  * @context: A #GUPnPContext
1187  * @server_path: Web server path where the file or folder is hosted
1188  *
1189  * Stop hosting the file or folder at @server_path.
1190  **/
1191 void
1192 gupnp_context_unhost_path (GUPnPContext *context,
1193                            const char   *server_path)
1194 {
1195         SoupServer *server;
1196         HostPathData *path_data;
1197         GList *node;
1198
1199         g_return_if_fail (GUPNP_IS_CONTEXT (context));
1200         g_return_if_fail (server_path != NULL);
1201
1202         server = gupnp_context_get_server (context);
1203
1204         node = g_list_find_custom (context->priv->host_path_datas,
1205                                    server_path,
1206                                    (GCompareFunc) path_compare_func);
1207         g_return_if_fail (node != NULL);
1208
1209         path_data = (HostPathData *) node->data;
1210         context->priv->host_path_datas = g_list_delete_link (
1211                         context->priv->host_path_datas,
1212                         node);
1213
1214         soup_server_remove_handler (server, server_path);
1215         host_path_data_free (path_data);
1216 }