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