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