2 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3 * Copyright (C) 2009 Nokia Corporation, all rights reserved.
5 * Author: Jorn Baayen <jorn@openedhand.com>
6 * Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
7 * <zeeshan.ali@nokia.com>
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.
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.
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., 59 Temple Place - Suite 330,
22 * Boston, MA 02111-1307, USA.
26 * SECTION:gupnp-context
27 * @short_description: Context object wrapping shared networking bits.
29 * #GUPnPContext wraps the networking bits that are used by the various
30 * GUPnP classes. It automatically starts a web server on demand.
32 * For debugging, it is possible to see the messages being sent and received by
33 * exporting #GUPNP_DEBUG.
43 #include <sys/utsname.h>
44 #include <sys/ioctl.h>
45 #include <sys/socket.h>
46 #include <sys/types.h>
48 #include <libsoup/soup-address.h>
49 #include <glib/gstdio.h>
51 #include "gupnp-context.h"
52 #include "gupnp-context-private.h"
53 #include "gupnp-marshal.h"
54 #include "gena-protocol.h"
55 #include "http-headers.h"
57 G_DEFINE_TYPE (GUPnPContext,
61 struct _GUPnPContextPrivate {
64 guint subscription_timeout;
68 SoupServer *server; /* Started on demand */
71 GList *host_path_datas;
79 PROP_SUBSCRIPTION_TIMEOUT
96 * Generates the default server ID.
101 struct utsname sysinfo;
105 return g_strdup_printf ("%s/%s UPnP/1.0 GUPnP/%s",
112 gupnp_context_init (GUPnPContext *context)
117 G_TYPE_INSTANCE_GET_PRIVATE (context,
119 GUPnPContextPrivate);
121 server_id = make_server_id ();
122 gssdp_client_set_server_id (GSSDP_CLIENT (context), server_id);
127 gupnp_context_constructor (GType type,
129 GObjectConstructParam *props)
132 GUPnPContext *context;
135 object = G_OBJECT_CLASS (gupnp_context_parent_class)->constructor
136 (type, n_props, props);
137 context = GUPNP_CONTEXT (object);
139 context->priv->session = soup_session_async_new_with_options
140 (SOUP_SESSION_IDLE_TIMEOUT,
142 SOUP_SESSION_ASYNC_CONTEXT,
143 gssdp_client_get_main_context (GSSDP_CLIENT (context)),
146 user_agent = g_strdup_printf ("%s GUPnP/" VERSION " DLNADOC/1.50",
147 g_get_application_name ()? : "");
148 g_object_set (context->priv->session,
149 SOUP_SESSION_USER_AGENT,
154 if (g_getenv ("GUPNP_DEBUG")) {
156 logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
157 soup_session_add_feature (context->priv->session,
158 SOUP_SESSION_FEATURE (logger));
165 gupnp_context_set_property (GObject *object,
170 GUPnPContext *context;
172 context = GUPNP_CONTEXT (object);
174 switch (property_id) {
176 context->priv->port = g_value_get_uint (value);
178 case PROP_SUBSCRIPTION_TIMEOUT:
179 context->priv->subscription_timeout = g_value_get_uint (value);
182 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
188 gupnp_context_get_property (GObject *object,
193 GUPnPContext *context;
195 context = GUPNP_CONTEXT (object);
197 switch (property_id) {
199 g_value_set_uint (value,
200 gupnp_context_get_port (context));
203 g_value_set_object (value,
204 gupnp_context_get_server (context));
207 g_value_set_object (value,
208 gupnp_context_get_session (context));
210 case PROP_SUBSCRIPTION_TIMEOUT:
211 g_value_set_uint (value,
212 gupnp_context_get_subscription_timeout
216 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
222 gupnp_context_dispose (GObject *object)
224 GUPnPContext *context;
225 GObjectClass *object_class;
227 context = GUPNP_CONTEXT (object);
229 if (context->priv->session) {
230 g_object_unref (context->priv->session);
231 context->priv->session = NULL;
234 if (context->priv->server) {
235 g_object_unref (context->priv->server);
236 context->priv->server = NULL;
239 while (context->priv->host_path_datas) {
242 data = (HostPathData *) context->priv->host_path_datas->data;
244 gupnp_context_unhost_path (context, data->server_path);
248 object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
249 object_class->dispose (object);
253 gupnp_context_finalize (GObject *object)
255 GUPnPContext *context;
256 GObjectClass *object_class;
258 context = GUPNP_CONTEXT (object);
260 g_free (context->priv->server_url);
263 object_class = G_OBJECT_CLASS (gupnp_context_parent_class);
264 object_class->finalize (object);
268 gupnp_context_class_init (GUPnPContextClass *klass)
270 GObjectClass *object_class;
272 object_class = G_OBJECT_CLASS (klass);
274 object_class->constructor = gupnp_context_constructor;
275 object_class->set_property = gupnp_context_set_property;
276 object_class->get_property = gupnp_context_get_property;
277 object_class->dispose = gupnp_context_dispose;
278 object_class->finalize = gupnp_context_finalize;
280 g_type_class_add_private (klass, sizeof (GUPnPContextPrivate));
285 * The port to run on. Set to 0 if you don't care what port to run on.
287 g_object_class_install_property
290 g_param_spec_uint ("port",
293 0, G_MAXUINT, SOUP_ADDRESS_ANY_PORT,
295 G_PARAM_CONSTRUCT_ONLY |
296 G_PARAM_STATIC_NAME |
297 G_PARAM_STATIC_NICK |
298 G_PARAM_STATIC_BLURB));
301 * GUPnPContext:server:
303 * The #SoupServer HTTP server used by GUPnP.
305 g_object_class_install_property
308 g_param_spec_object ("server",
310 "SoupServer HTTP server",
313 G_PARAM_STATIC_NAME |
314 G_PARAM_STATIC_NICK |
315 G_PARAM_STATIC_BLURB));
318 * GUPnPContext:session:
320 * The #SoupSession object used by GUPnP.
322 g_object_class_install_property
325 g_param_spec_object ("session",
327 "SoupSession object",
330 G_PARAM_STATIC_NAME |
331 G_PARAM_STATIC_NICK |
332 G_PARAM_STATIC_BLURB));
335 * GUPnPContext:subscription-timeout:
337 * The preferred subscription timeout: the number of seconds after
338 * which subscriptions are renewed. Set to '0' if subscriptions
339 * are never to time out.
341 g_object_class_install_property
343 PROP_SUBSCRIPTION_TIMEOUT,
344 g_param_spec_uint ("subscription-timeout",
345 "Subscription timeout",
346 "Subscription timeout",
349 GENA_DEFAULT_TIMEOUT,
351 G_PARAM_CONSTRUCT_ONLY |
352 G_PARAM_STATIC_NAME |
353 G_PARAM_STATIC_NICK |
354 G_PARAM_STATIC_BLURB));
358 * gupnp_context_get_session:
359 * @context: A #GUPnPContext
361 * Get the #SoupSession object that GUPnP is using.
363 * Return value: (transfer none): The #SoupSession used by GUPnP. Do not unref
364 * this when finished.
367 gupnp_context_get_session (GUPnPContext *context)
369 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
371 return context->priv->session;
375 * Default server handler: Return 404 not found.
378 default_server_handler (SoupServer *server,
382 SoupClientContext *client,
385 soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
389 * gupnp_context_get_server:
390 * @context: A #GUPnPContext
392 * Get the #SoupServer HTTP server that GUPnP is using.
394 * Return value: The #SoupServer used by GUPnP. Do not unref this when finished.
397 gupnp_context_get_server (GUPnPContext *context)
399 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
401 if (context->priv->server == NULL) {
404 ip = gssdp_client_get_host_ip (GSSDP_CLIENT (context));
405 SoupAddress *addr = soup_address_new (ip, context->priv->port);
406 soup_address_resolve_sync (addr, NULL);
408 context->priv->server = soup_server_new
411 SOUP_SERVER_ASYNC_CONTEXT,
412 gssdp_client_get_main_context (GSSDP_CLIENT (context)),
413 SOUP_SERVER_INTERFACE,
416 g_object_unref (addr);
418 soup_server_add_handler (context->priv->server, NULL,
419 default_server_handler, context,
422 soup_server_run_async (context->priv->server);
425 return context->priv->server;
429 * Makes an URL that refers to our server.
432 make_server_url (GUPnPContext *context)
437 /* What port are we running on? */
438 server = gupnp_context_get_server (context);
439 port = soup_server_get_port (server);
441 /* Put it all together */
442 return g_strdup_printf
444 gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
449 _gupnp_context_get_server_url (GUPnPContext *context)
451 if (context->priv->server_url == NULL)
452 context->priv->server_url = make_server_url (context);
454 return (const char *) context->priv->server_url;
459 * @main_context: A #GMainContext, or %NULL to use the default one
460 * @interface: The network interface to use, or %NULL to auto-detect.
461 * @port: Port to run on, or 0 if you don't care what port is used.
462 * @error: A location to store a #GError, or %NULL
464 * Create a new #GUPnPContext with the specified @main_context, @interface and
467 * Return value: A new #GUPnPContext object, or %NULL on an error
470 gupnp_context_new (GMainContext *main_context,
471 const char *interface,
475 return g_object_new (GUPNP_TYPE_CONTEXT,
476 "main-context", main_context,
477 "interface", interface,
484 * gupnp_context_get_host_ip:
485 * @context: A #GUPnPContext
487 * Get the IP address we advertise ourselves as using.
489 * Return value: The IP address. This string should not be freed.
491 * Deprecated:0.12.7: The "host-ip" property has moved to the base class
492 * #GSSDPClient so newer applications should use
493 * #gssdp_client_get_host_ip instead.
496 gupnp_context_get_host_ip (GUPnPContext *context)
498 return gssdp_client_get_host_ip (GSSDP_CLIENT (context));
502 * gupnp_context_get_port:
503 * @context: A #GUPnPContext
505 * Get the port that the SOAP server is running on.
507 * Return value: The port the SOAP server is running on.
510 gupnp_context_get_port (GUPnPContext *context)
514 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);
516 server = gupnp_context_get_server (context);
517 return soup_server_get_port (server);
521 * gupnp_context_set_subscription_timeout:
522 * @context: A #GUPnPContext
523 * @timeout: Event subscription timeout in seconds
525 * Sets the event subscription timeout to @timeout. Use 0 if you don't
526 * want subscriptions to time out. Note that any client side subscriptions
527 * will automatically be renewed.
530 gupnp_context_set_subscription_timeout (GUPnPContext *context,
533 g_return_if_fail (GUPNP_IS_CONTEXT (context));
535 context->priv->subscription_timeout = timeout;
537 g_object_notify (G_OBJECT (context), "subscription-timeout");
541 * gupnp_context_get_subscription_timeout:
542 * @context: A #GUPnPContext
544 * Get the event subscription timeout (in seconds), or 0 meaning there is no
547 * Return value: The event subscription timeout in seconds.
550 gupnp_context_get_subscription_timeout (GUPnPContext *context)
552 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), 0);
554 return context->priv->subscription_timeout;
557 /* Construct a local path from @requested path, removing the last slash
558 * if any to make sure we append the locale suffix in a canonical way. */
560 construct_local_path (const char *requested_path,
561 const char *user_agent,
562 HostPathData *host_path_data)
570 if (user_agent != NULL) {
573 for (node = host_path_data->user_agents;
580 if (g_regex_match (agent->regex,
584 local_path = agent->local_path;
589 if (local_path == NULL)
590 local_path = host_path_data->local_path;
592 if (!requested_path || *requested_path == 0)
593 return g_strdup (local_path);
595 if (*requested_path != '/')
596 return NULL; /* Absolute paths only */
598 str = g_string_new (local_path);
600 /* Skip the length of the path relative to which @requested_path
602 requested_path += strlen (host_path_data->server_path);
604 /* Strip the last slashes to make sure we append the locale suffix
605 * in a canonical way. */
606 len = strlen (requested_path);
607 while (requested_path[len - 1] == '/')
610 g_string_append_len (str,
614 return g_string_free (str, FALSE);
617 /* Append locale suffix to @local_path. */
619 append_locale (const char *local_path, GList *locales)
622 return g_strdup (local_path);
624 return g_strdup_printf ("%s.%s",
626 (char *) locales->data);
629 /* Redirect @msg to the same URI, but with a slash appended. */
631 redirect_to_folder (SoupMessage *msg)
633 char *uri, *redir_uri;
635 uri = soup_uri_to_string (soup_message_get_uri (msg),
637 redir_uri = g_strdup_printf ("%s/", uri);
638 soup_message_headers_append (msg->response_headers,
639 "Location", redir_uri);
640 soup_message_set_status (msg,
641 SOUP_STATUS_MOVED_PERMANENTLY);
646 /* Serve @path. Note that we do not need to check for path including bogus
647 * '..' as libsoup does this for us. */
649 host_path_handler (SoupServer *server,
653 SoupClientContext *client,
656 char *local_path, *path_to_open;
659 GList *locales, *orig_locales;
660 GMappedFile *mapped_file;
662 HostPathData *host_path_data;
663 const char *user_agent;
669 host_path_data = (HostPathData *) user_data;
671 if (msg->method != SOUP_METHOD_GET &&
672 msg->method != SOUP_METHOD_HEAD) {
673 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
678 user_agent = soup_message_headers_get_one (msg->request_headers,
681 /* Construct base local path */
682 local_path = construct_local_path (path, user_agent, host_path_data);
684 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
689 /* Get preferred locales */
690 orig_locales = locales = http_request_get_accept_locales (msg);
693 /* Add locale suffix if available */
694 path_to_open = append_locale (local_path, locales);
696 /* See what we've got */
697 if (g_stat (path_to_open, &st) == -1) {
699 soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
700 else if (errno == ENOENT) {
702 g_free (path_to_open);
704 locales = locales->next;
708 soup_message_set_status (msg,
709 SOUP_STATUS_NOT_FOUND);
711 soup_message_set_status
712 (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
717 /* Handle directories */
718 if (S_ISDIR (st.st_mode)) {
719 if (!g_str_has_suffix (path, "/")) {
720 redirect_to_folder (msg);
725 /* This incorporates the locale portion in the folder name
728 local_path = g_build_filename (path_to_open,
732 g_free (path_to_open);
739 mapped_file = g_mapped_file_new (path_to_open, FALSE, &error);
741 if (mapped_file == NULL) {
742 g_warning ("Unable to map file %s: %s",
743 path_to_open, error->message);
745 g_error_free (error);
747 soup_message_set_status (msg,
748 SOUP_STATUS_INTERNAL_SERVER_ERROR);
753 /* Handle method (GET or HEAD) */
754 status = SOUP_STATUS_OK;
756 if (msg->method == SOUP_METHOD_GET) {
757 gsize offset, length;
767 if (!http_request_get_range (msg,
771 soup_message_set_status
773 SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
778 if (have_range && (length > st.st_size - offset ||
780 (off_t) offset >= st.st_size)) {
781 soup_message_set_status
783 SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
788 /* Add requested content */
789 buffer = soup_buffer_new_with_owner
790 (g_mapped_file_get_contents (mapped_file) + offset,
793 (GDestroyNotify) g_mapped_file_unref);
795 soup_message_body_append_buffer (msg->response_body, buffer);
797 soup_buffer_free (buffer);
801 http_response_set_content_range (msg,
806 status = SOUP_STATUS_PARTIAL_CONTENT;
809 } else if (msg->method == SOUP_METHOD_HEAD) {
812 length = g_strdup_printf ("%lu", (gulong) st.st_size);
813 soup_message_headers_append (msg->response_headers,
819 soup_message_set_status (msg,
820 SOUP_STATUS_METHOD_NOT_ALLOWED);
825 /* Set Content-Type */
826 http_response_set_content_type (msg,
828 (guchar *) g_mapped_file_get_contents
832 /* Set Content-Language */
834 http_response_set_content_locale (msg, locales->data);
836 /* Set Accept-Ranges */
837 soup_message_headers_append (msg->response_headers,
842 soup_message_set_status (msg, status);
846 g_free (path_to_open);
849 while (orig_locales) {
850 g_free (orig_locales->data);
851 orig_locales = g_list_delete_link (orig_locales, orig_locales);
856 user_agent_new (const char *local_path,
861 agent = g_slice_new0 (UserAgent);
863 agent->local_path = g_strdup (local_path);
864 agent->regex = g_regex_ref (regex);
870 user_agent_free (UserAgent *agent)
872 g_free (agent->local_path);
873 g_regex_unref (agent->regex);
875 g_slice_free (UserAgent, agent);
878 static HostPathData *
879 host_path_data_new (const char *local_path,
880 const char *server_path)
882 HostPathData *path_data;
884 path_data = g_slice_new0 (HostPathData);
886 path_data->local_path = g_strdup (local_path);
887 path_data->server_path = g_strdup (server_path);
893 host_path_data_free (HostPathData *path_data)
895 g_free (path_data->local_path);
896 g_free (path_data->server_path);
897 while (path_data->user_agents) {
900 agent = path_data->user_agents->data;
902 user_agent_free (agent);
904 path_data->user_agents = g_list_delete_link (
905 path_data->user_agents,
906 path_data->user_agents);
909 g_slice_free (HostPathData, path_data);
913 * gupnp_context_host_path:
914 * @context: A #GUPnPContext
915 * @local_path: Path to the local file or folder to be hosted
916 * @server_path: Web server path where @local_path should be hosted
918 * Start hosting @local_path at @server_path. Files with the path
919 * @local_path.LOCALE (if they exist) will be served up when LOCALE is
920 * specified in the request's Accept-Language header.
923 gupnp_context_host_path (GUPnPContext *context,
924 const char *local_path,
925 const char *server_path)
928 HostPathData *path_data;
930 g_return_if_fail (GUPNP_IS_CONTEXT (context));
931 g_return_if_fail (local_path != NULL);
932 g_return_if_fail (server_path != NULL);
934 server = gupnp_context_get_server (context);
936 path_data = host_path_data_new (local_path,
939 soup_server_add_handler (server,
945 context->priv->host_path_datas =
946 g_list_append (context->priv->host_path_datas,
951 path_compare_func (HostPathData *path_data,
952 const char *server_path)
954 return strcmp (path_data->server_path, server_path);
958 * gupnp_context_host_path_for_agent:
959 * @context: A #GUPnPContext
960 * @local_path: Path to the local file or folder to be hosted
961 * @server_path: Web server path already being hosted
962 * @user_agent: The user-agent as a #GRegex.
964 * Use this method to serve different local path to specific user-agent(s). The
965 * path @server_path must already be hosted by @context.
967 * Return value: %TRUE on success, %FALSE otherwise.
970 gupnp_context_host_path_for_agent (GUPnPContext *context,
971 const char *local_path,
972 const char *server_path,
977 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), FALSE);
978 g_return_val_if_fail (local_path != NULL, FALSE);
979 g_return_val_if_fail (server_path != NULL, FALSE);
980 g_return_val_if_fail (user_agent != NULL, FALSE);
982 node = g_list_find_custom (context->priv->host_path_datas,
984 (GCompareFunc) path_compare_func);
986 HostPathData *path_data;
989 path_data = (HostPathData *) node->data;
990 agent = user_agent_new (local_path, user_agent);
992 path_data->user_agents = g_list_append (path_data->user_agents,
1001 * gupnp_context_unhost_path:
1002 * @context: A #GUPnPContext
1003 * @server_path: Web server path where the file or folder is hosted
1005 * Stop hosting the file or folder at @server_path.
1008 gupnp_context_unhost_path (GUPnPContext *context,
1009 const char *server_path)
1012 HostPathData *path_data;
1015 g_return_if_fail (GUPNP_IS_CONTEXT (context));
1016 g_return_if_fail (server_path != NULL);
1018 server = gupnp_context_get_server (context);
1020 node = g_list_find_custom (context->priv->host_path_datas,
1022 (GCompareFunc) path_compare_func);
1023 g_return_if_fail (node != NULL);
1025 path_data = (HostPathData *) node->data;
1026 context->priv->host_path_datas = g_list_delete_link (
1027 context->priv->host_path_datas,
1030 soup_server_remove_handler (server, server_path);
1031 host_path_data_free (path_data);