/* * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd. * * Author: Jorn Baayen * * SPDX-License-Identifier: LGPL-2.1-or-later * */ #define G_LOG_DOMAIN "gupnp-service-proxy" #include #include #include #include #include #include #include "gena-protocol.h" #include "gupnp-context-private.h" #include "gupnp-error-private.h" #include "gupnp-error.h" #include "gupnp-service-proxy.h" #include "gupnp-service-proxy-action-private.h" #include "gupnp-types.h" #include "gvalue-util.h" #include "http-headers.h" #include "xml-util.h" struct _GUPnPServiceProxyPrivate { gboolean subscribed; char *path; /* Path to this proxy */ // Credentials char *user; char *password; char *sid; /* Subscription ID */ GSource *subscription_timeout_src; guint32 seq; /* Event sequence number */ GHashTable *notify_hash; // GCancellable that is used to all SoupMessages that are neither // notifies nor proxy calls GCancellable *pending_messages; GQueue *pending_notifies; /* Pending notifications to be sent (xmlDoc) */ GSource *notify_idle_src; /* Idle handler src of notification emiter */ }; typedef struct _GUPnPServiceProxyPrivate GUPnPServiceProxyPrivate; /** * GUPnPServiceProxy: * * Proxy class for remote services. * * #GUPnPServiceProxy sends commands to a remote UPnP service and handles * incoming event notifications. */ G_DEFINE_TYPE_WITH_PRIVATE (GUPnPServiceProxy, gupnp_service_proxy, GUPNP_TYPE_SERVICE_INFO) enum { PROP_0, PROP_SUBSCRIBED }; enum { SUBSCRIPTION_LOST, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; typedef struct { GType type; /* type of the variable this notification is for */ GList *callbacks; /* list of CallbackData */ GList *next_emit; /* pointer into callbacks as which callback to call next*/ } NotifyData; typedef struct { GUPnPServiceProxyNotifyCallback callback; GDestroyNotify notify; gpointer user_data; } NotifyCallbackData; typedef struct { char *sid; guint32 seq; xmlDoc *doc; } EmitNotifyData; static void subscribe_got_response (GObject *source, GAsyncResult *res, gpointer user_data); static void subscribe (GUPnPServiceProxy *proxy); static void unsubscribe (GUPnPServiceProxy *proxy); static void callback_data_free (NotifyCallbackData *data) { if (data->notify) data->notify (data->user_data); g_slice_free (NotifyCallbackData, data); } static void notify_data_free (NotifyData *data) { g_list_free_full (data->callbacks, (GDestroyNotify) callback_data_free); g_slice_free (NotifyData, data); } /* Steals doc reference */ static EmitNotifyData * emit_notify_data_new (const char *sid, guint32 seq, xmlDoc *doc) { EmitNotifyData *data; data = g_slice_new (EmitNotifyData); data->sid = g_strdup (sid); data->seq = seq; data->doc = doc; return data; } static void emit_notify_data_free (EmitNotifyData *data) { g_free (data->sid); xmlFreeDoc (data->doc); g_slice_free (EmitNotifyData, data); } static void gupnp_service_proxy_init (GUPnPServiceProxy *proxy) { static int proxy_counter = 0; GUPnPServiceProxyPrivate *priv; /* Generate unique path */ priv = gupnp_service_proxy_get_instance_private (proxy); priv->path = g_strdup_printf ("/ServiceProxy%d", proxy_counter); proxy_counter++; /* Set up notify hash */ priv->notify_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) notify_data_free); priv->pending_messages = g_cancellable_new (); priv->pending_notifies = g_queue_new (); } static void gupnp_service_proxy_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GUPnPServiceProxy *proxy; proxy = GUPNP_SERVICE_PROXY (object); switch (property_id) { case PROP_SUBSCRIBED: gupnp_service_proxy_set_subscribed (proxy, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gupnp_service_proxy_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GUPnPServiceProxy *proxy; GUPnPServiceProxyPrivate *priv; proxy = GUPNP_SERVICE_PROXY (object); priv = gupnp_service_proxy_get_instance_private (proxy); switch (property_id) { case PROP_SUBSCRIBED: g_value_set_boolean (value, priv->subscribed); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gupnp_service_proxy_dispose (GObject *object) { GUPnPServiceProxy *proxy; GUPnPServiceProxyPrivate *priv; GObjectClass *object_class; GUPnPContext *context; // SoupSession *session; proxy = GUPNP_SERVICE_PROXY (object); priv = gupnp_service_proxy_get_instance_private (proxy); /* Unsubscribe */ if (priv->subscribed) { unsubscribe (proxy); priv->subscribed = FALSE; } context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); /* Remove server handler */ if (context) { SoupServer *server; server = gupnp_context_get_server (context); soup_server_remove_handler (server, priv->path); } if (priv->pending_messages) g_cancellable_cancel (priv->pending_messages); g_clear_object (&priv->pending_messages); /* Cancel pending notifications */ g_clear_pointer (&priv->notify_idle_src, g_source_destroy); g_queue_free_full (g_steal_pointer(&priv->pending_notifies), (GDestroyNotify) emit_notify_data_free); /* Call super */ object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class); object_class->dispose (object); } static void gupnp_service_proxy_finalize (GObject *object) { GUPnPServiceProxy *proxy; GUPnPServiceProxyPrivate *priv; GObjectClass *object_class; proxy = GUPNP_SERVICE_PROXY (object); priv = gupnp_service_proxy_get_instance_private (proxy); g_free (priv->path); g_hash_table_destroy (priv->notify_hash); /* Call super */ object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class); object_class->finalize (object); } static void gupnp_service_proxy_class_init (GUPnPServiceProxyClass *klass) { GObjectClass *object_class; object_class = G_OBJECT_CLASS (klass); object_class->set_property = gupnp_service_proxy_set_property; object_class->get_property = gupnp_service_proxy_get_property; object_class->dispose = gupnp_service_proxy_dispose; object_class->finalize = gupnp_service_proxy_finalize; /** * GUPnPServiceProxy:subscribed: * * Whether we are subscribed to this service. **/ g_object_class_install_property (object_class, PROP_SUBSCRIBED, g_param_spec_boolean ("subscribed", "Subscribed", "Whether we are subscribed to this " "service", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); /** * GUPnPServiceProxy::subscription-lost: * @proxy: The #GUPnPServiceProxy that received the signal * @error: (type GError):A pointer to a #GError describing why the subscription has * been lost * * Emitted whenever the subscription to this service has been lost due * to an error condition. **/ signals[SUBSCRIPTION_LOST] = g_signal_new ("subscription-lost", GUPNP_TYPE_SERVICE_PROXY, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GUPnPServiceProxyClass, subscription_lost), NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); } static gboolean on_authenticate (SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data) { GUPnPServiceProxy *proxy = user_data; GUPnPServiceProxyPrivate *priv; priv = gupnp_service_proxy_get_instance_private (proxy); if (!retrying && priv->user != NULL && priv->password != NULL) { soup_auth_authenticate (auth, priv->user, priv->password); return FALSE; } return FALSE; } static void on_restarted (SoupMessage *msg, gpointer user_data) { GUPnPServiceProxyAction *action = user_data; g_autoptr (GBytes) body = NULL; const char *service_type; service_type = gupnp_service_info_get_service_type (GUPNP_SERVICE_INFO (action->proxy)); gupnp_service_proxy_action_serialize (action, service_type); body = g_string_free_to_bytes (action->msg_str); soup_message_set_request_body_from_bytes (msg, "text/xml; charset=\"utf-8\"", body); action->msg_str = NULL; } /* Begins a basic action message */ static gboolean prepare_action_msg (GUPnPServiceProxy *proxy, GUPnPServiceProxyAction *action, const char *method, GError **error) { char *control_url, *full_action; const char *service_type; /* Make sure we have a service type */ service_type = gupnp_service_info_get_service_type (GUPNP_SERVICE_INFO (proxy)); if (service_type == NULL) { g_propagate_error (error, g_error_new (GUPNP_SERVER_ERROR, GUPNP_SERVER_ERROR_OTHER, "No service type defined")); return FALSE; } /* Create message */ control_url = gupnp_service_info_get_control_url (GUPNP_SERVICE_INFO (proxy)); if (control_url == NULL) { g_propagate_error ( error, g_error_new (GUPNP_SERVER_ERROR, GUPNP_SERVER_ERROR_INVALID_URL, "No valid control URL defined")); return FALSE; } GUPnPContext *context = NULL; char *local_control_url = NULL; context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); local_control_url = gupnp_context_rewrite_uri (context, control_url); g_free (control_url); g_clear_object (&action->msg); action->msg = soup_message_new (method, local_control_url); g_signal_connect_object (G_OBJECT (action->msg), "authenticate", G_CALLBACK (on_authenticate), G_OBJECT (proxy), 0); g_signal_connect (G_OBJECT (action->msg), "restarted", G_CALLBACK (on_restarted), action); g_free (local_control_url); SoupMessageHeaders *headers = soup_message_get_request_headers (action->msg); /* Specify action */ full_action = g_strdup_printf ("\"%s#%s\"", service_type, action->name); // This is internal API, it should either be that constant or the string "M-POST" if (method == SOUP_METHOD_POST) { soup_message_headers_append (headers, "SOAPAction", full_action); } else { soup_message_headers_append (headers, "s-SOAPAction", full_action); soup_message_headers_append ( headers, "Man", "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=s"); } g_free (full_action); /* Specify language */ http_request_set_accept_language (action->msg); /* Accept gzip encoding */ soup_message_headers_append (headers, "Accept-Encoding", "gzip"); gupnp_service_proxy_action_serialize (action, service_type); GBytes *body = g_string_free_to_bytes (action->msg_str); soup_message_set_request_body_from_bytes (action->msg, "text/xml; charset=\"utf-8\"", body); g_bytes_unref (body); action->msg_str = NULL; action->proxy = proxy; g_object_add_weak_pointer (G_OBJECT (proxy), (gpointer *) &(action->proxy)); return TRUE; } static void gupnp_service_proxy_action_queue_task (GTask *task); static void action_task_got_response (GObject *source, GAsyncResult *res, gpointer user_data) { GTask *task = G_TASK (user_data); GError *error = NULL; GUPnPServiceProxyAction *action = (GUPnPServiceProxyAction *) g_task_get_task_data (task); action->response = soup_session_send_and_read_finish (SOUP_SESSION (source), res, &error); if (error != NULL) { action->error = g_error_copy (error); g_task_return_error (task, error); g_object_unref (task); return; } switch (soup_message_get_status (action->msg)) { case SOUP_STATUS_METHOD_NOT_ALLOWED: if (g_str_equal (soup_message_get_method (action->msg), "POST")) { g_debug ("POST returned with METHOD_NOT_ALLOWED, " "trying with M-POST"); g_bytes_unref (action->response); if (!prepare_action_msg (action->proxy, action, "M-POST", &error)) { g_task_return_error (task, error); g_object_unref (task); } else { gupnp_service_proxy_action_queue_task (task); } } else { g_task_return_new_error (task, GUPNP_SERVER_ERROR, GUPNP_SERVER_ERROR_OTHER, "Server does not allow any POST messages"); g_object_unref (task); } break; default: gupnp_service_proxy_action_check_response (action); if (action->error != NULL) { g_task_return_error (task, g_error_copy (action->error)); } else { g_task_return_pointer (task, g_task_get_task_data (task), NULL); } g_object_unref (task); break; } } static void gupnp_service_proxy_action_queue_task (GTask *task) { GUPnPContext *context; SoupSession *session; GUPnPServiceProxyAction *action = g_task_get_task_data (task); /* Send the message */ context = gupnp_service_info_get_context ( GUPNP_SERVICE_INFO (action->proxy)); session = gupnp_context_get_session (context); soup_session_send_and_read_async ( session, action->msg, G_PRIORITY_DEFAULT, g_task_get_cancellable (task), (GAsyncReadyCallback) action_task_got_response, task); action->pending = TRUE; } /** * gupnp_service_proxy_add_notify: (skip) * @proxy: A #GUPnPServiceProxy * @variable: The variable to add notification for * @type: The type of the variable * @callback: (scope async): The callback to call when @variable changes * @user_data: User data for @callback * * Sets up @callback to be called whenever a change notification for * @variable is recieved. * * Return value: %TRUE on success. **/ gboolean gupnp_service_proxy_add_notify (GUPnPServiceProxy *proxy, const char *variable, GType type, GUPnPServiceProxyNotifyCallback callback, gpointer user_data) { return gupnp_service_proxy_add_notify_full (proxy, variable, type, callback, user_data, NULL); } /** * gupnp_service_proxy_add_notify_full: (rename-to gupnp_service_proxy_add_notify) * @proxy: A #GUPnPServiceProxy * @variable: The variable to add notification for * @type: The type of the variable * @callback: (scope notified): The callback to call when @variable changes * @user_data: User data for @callback * @notify: (allow-none): Function to call when the notification is removed, or %NULL * * Sets up @callback to be called whenever a change notification for * @variable is recieved. * * Since: 0.20.12 * * Return value: %TRUE on success. **/ gboolean gupnp_service_proxy_add_notify_full (GUPnPServiceProxy *proxy, const char *variable, GType type, GUPnPServiceProxyNotifyCallback callback, gpointer user_data, GDestroyNotify notify) { NotifyData *data; NotifyCallbackData *callback_data; GUPnPServiceProxyPrivate *priv; g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE); g_return_val_if_fail (variable, FALSE); g_return_val_if_fail (type, FALSE); g_return_val_if_fail (callback, FALSE); priv = gupnp_service_proxy_get_instance_private (proxy); /* See if we already have notifications set up for this variable */ data = g_hash_table_lookup (priv->notify_hash, variable); if (data == NULL) { /* No, create one */ data = g_slice_new (NotifyData); data->type = type; data->callbacks = NULL; data->next_emit = NULL; g_hash_table_insert (priv->notify_hash, g_strdup (variable), data); } else { /* Yes, check that everything is sane .. */ if (data->type != type) { g_warning ("A notification already exists for %s, but " "has type %s, not %s.", variable, g_type_name (data->type), g_type_name (type)); return FALSE; } } /* Append callback */ callback_data = g_slice_new (NotifyCallbackData); callback_data->callback = callback; callback_data->user_data = user_data; callback_data->notify = notify; data->callbacks = g_list_append (data->callbacks, callback_data); if (data->next_emit == NULL) data->next_emit = g_list_last (data->callbacks); return TRUE; } /** * gupnp_service_proxy_add_raw_notify: * @proxy: A #GUPnPServiceProxy * @callback: (scope notified): The callback to call when the peer issues any * variable notification. * @user_data: User data for @callback * @notify: (allow-none): A #GDestroyNotify for @user_data * * Get a notification for anything that happens on the peer. * * @value in @callback will be of type G_TYPE_POINTER and contain the pre-parsed * [type@libxml2.Doc]. Do NOT free or modify this document. * * Return value: %TRUE on success. * * Since: 0.20.12 **/ gboolean gupnp_service_proxy_add_raw_notify (GUPnPServiceProxy *proxy, GUPnPServiceProxyNotifyCallback callback, gpointer user_data, GDestroyNotify notify) { return gupnp_service_proxy_add_notify_full (proxy, "*", G_TYPE_NONE, callback, user_data, notify); } /** * gupnp_service_proxy_remove_notify: * @proxy: A #GUPnPServiceProxy * @variable: The variable to add notification for * @callback: (scope call): The callback to call when @variable changes * @user_data: User data for @callback * * Cancels the variable change notification for @callback and @user_data. * * Up to version 0.20.9 this function must not be called directlya or * indirectly from a #GUPnPServiceProxyNotifyCallback associated with this * service proxy, even if it is for another variable. In later versions such * calls are allowed. * * Return value: %TRUE on success. **/ gboolean gupnp_service_proxy_remove_notify (GUPnPServiceProxy *proxy, const char *variable, GUPnPServiceProxyNotifyCallback callback, gpointer user_data) { NotifyData *data; gboolean found; GList *l; GUPnPServiceProxyPrivate *priv; g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE); g_return_val_if_fail (variable, FALSE); g_return_val_if_fail (callback, FALSE); priv = gupnp_service_proxy_get_instance_private (proxy); /* Look up NotifyData for variable */ data = g_hash_table_lookup (priv->notify_hash, variable); if (data == NULL) { g_warning ("No notifications found for variable %s", variable); return FALSE; } /* Look up our callback-user_data pair */ found = FALSE; for (l = data->callbacks; l; l = l->next) { NotifyCallbackData *callback_data; callback_data = l->data; if (callback_data->callback == callback && callback_data->user_data == user_data) { /* Gotcha! */ callback_data_free (callback_data); if (data->next_emit == l) data->next_emit = data->next_emit->next; data->callbacks = g_list_delete_link (data->callbacks, l); if (data->callbacks == NULL) { /* No callbacks left: Remove from hash */ g_hash_table_remove (priv->notify_hash, variable); } found = TRUE; break; } } if (found == FALSE) g_warning ("No such callback-user_data pair was found"); return found; } /** * gupnp_service_proxy_remove_raw_notify: * @proxy: A #GUPnPServiceProxy * @callback: (scope call): The callback to call when @variable changes * @user_data: User data for @callback * * Cancels the variable change notification for @callback and @user_data. * * This function must not be called directly or indirectly from a * #GUPnPServiceProxyNotifyCallback associated with this service proxy, even * if it is for another variable. * * Return value: %TRUE on success. **/ gboolean gupnp_service_proxy_remove_raw_notify (GUPnPServiceProxy *proxy, GUPnPServiceProxyNotifyCallback callback, gpointer user_data) { return gupnp_service_proxy_remove_notify (proxy, "*", callback, user_data); } static void emit_notification (GUPnPServiceProxy *proxy, xmlNode *var_node) { NotifyData *data; GValue value = {0, }; GList *l; GUPnPServiceProxyPrivate *priv; priv = gupnp_service_proxy_get_instance_private (proxy); data = g_hash_table_lookup (priv->notify_hash, var_node->name); if (data == NULL) return; /* Make a GValue of the desired type */ g_value_init (&value, data->type); if (!gvalue_util_set_value_from_xml_node (&value, var_node)) { g_value_unset (&value); return; } /* Call callbacks. Note that data->next_emit may change if * callback calls remove_notify() or add_notify() */ for (l = data->callbacks; l; l = data->next_emit) { NotifyCallbackData *callback_data; callback_data = l->data; data->next_emit = l->next; callback_data->callback (proxy, (const char *) var_node->name, &value, callback_data->user_data); } /* Cleanup */ g_value_unset (&value); } static void emit_notifications_for_doc (GUPnPServiceProxy *proxy, xmlDoc *doc) { NotifyData *data = NULL; xmlNode *node; GUPnPServiceProxyPrivate *priv; node = xmlDocGetRootElement (doc); /* Iterate over all provided properties */ for (node = node->children; node; node = node->next) { xmlNode *var_node; /* Although according to the UPnP specs, there should be only * one variable node inside a 'property' node, we still need to * entertain the possibility of multiple variables inside it to * be compatible with implementations using older GUPnP. */ for (var_node = node->children; var_node; var_node = var_node->next) { if (strcmp ((char *) node->name, "property") == 0) emit_notification (proxy, var_node); } } priv = gupnp_service_proxy_get_instance_private (proxy); data = g_hash_table_lookup (priv->notify_hash, "*"); if (data != NULL) { GValue value = {0, }; GList *it = NULL; g_value_init (&value, G_TYPE_POINTER); g_value_set_pointer (&value, doc); for (it = data->callbacks; it != NULL; it = it->next) { NotifyCallbackData *callback_data = it->data; callback_data->callback (proxy, "*", &value, callback_data->user_data); } g_value_unset (&value); } } /* Emit pending notifications. See comment below on why we do this. */ static gboolean emit_notifications (gpointer user_data) { GUPnPServiceProxy *proxy = user_data; GUPnPServiceProxyPrivate *priv; GList *pending_notify; gboolean resubscribe = FALSE; g_assert (user_data); priv = gupnp_service_proxy_get_instance_private (proxy); if (priv->sid == NULL) /* No SID */ if (G_LIKELY (priv->subscribed)) /* subscription in progress, delay emision! */ return TRUE; for (pending_notify = priv->pending_notifies->head; pending_notify != NULL; pending_notify = pending_notify->next) { EmitNotifyData *emit_notify_data; emit_notify_data = pending_notify->data; if (emit_notify_data->seq > priv->seq) { /* Error procedure on missed event according to * UDA 1.0, section 4.2, §5: * Re-subscribe to get a new SID and SEQ */ resubscribe = TRUE; break; } /* Increment our own event sequence number */ /* UDA 1.0, section 4.2, §3: To prevent overflow, SEQ is set to * 1, NOT 0, when encountering G_MAXUINT32. SEQ == 0 always * indicates the initial event message. */ if (priv->seq < G_MAXUINT32) priv->seq++; else priv->seq = 1; if (G_LIKELY (priv->sid != NULL && strcmp (emit_notify_data->sid, priv->sid) == 0)) /* Our SID, entertain! */ emit_notifications_for_doc (proxy, emit_notify_data->doc); } /* Cleanup */ g_queue_clear_full (priv->pending_notifies, (GDestroyNotify) emit_notify_data_free); if (resubscribe) { unsubscribe (proxy); subscribe (proxy); } priv->notify_idle_src = NULL; return FALSE; } /* * HTTP server received a message. Handle, if this was a NOTIFY * message with our SID. */ static void server_handler (G_GNUC_UNUSED SoupServer *soup_server, SoupServerMessage *msg, G_GNUC_UNUSED const char *server_path, G_GNUC_UNUSED GHashTable *query, gpointer user_data) { GUPnPServiceProxy *proxy; GUPnPServiceProxyPrivate *priv; const char *hdr, *nt, *nts; guint32 seq; guint64 seq_parsed; xmlDoc *doc; xmlNode *node; EmitNotifyData *emit_notify_data; proxy = GUPNP_SERVICE_PROXY (user_data); const char *method = soup_server_message_get_method (msg); SoupMessageHeaders *request_headers = soup_server_message_get_request_headers (msg); if (strcmp (method, GENA_METHOD_NOTIFY) != 0) { /* We don't implement this method */ soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, "Method not supported"); return; } nt = soup_message_headers_get_one (request_headers, "NT"); nts = soup_message_headers_get_one (request_headers, "NTS"); if (nt == NULL || nts == NULL) { /* Required header is missing */ soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, "NT or NTS is missing"); return; } if (strcmp (nt, "upnp:event") != 0 || strcmp (nts, "upnp:propchange") != 0) { /* Unexpected header content */ soup_server_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED, "Unexpected NT or NTS"); return; } hdr = soup_message_headers_get_one (request_headers, "SEQ"); if (hdr == NULL) { /* No SEQ header */ soup_server_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED, "SEQ missing"); return; } errno = 0; seq_parsed = strtoul (hdr, NULL, 10); if (errno != 0 || seq_parsed > G_MAXUINT32) { /* Invalid SEQ header value */ soup_server_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED, "SEQ invalid"); return; } seq = (guint32) seq_parsed; hdr = soup_message_headers_get_one (request_headers, "SID"); if (hdr == NULL || strlen (hdr) <= strlen ("uuid:") || strncmp (hdr, "uuid:", strlen ("uuid:")) != 0) { /* No SID */ soup_server_message_set_status ( msg, SOUP_STATUS_PRECONDITION_FAILED, "SID header missing or malformed"); return; } SoupMessageBody *request_body = soup_server_message_get_request_body (msg); /* Parse the actual XML message content */ doc = xmlReadMemory (request_body->data, request_body->length, NULL, NULL, XML_PARSE_NONET | XML_PARSE_RECOVER); if (doc == NULL) { /* Failed */ g_warning ("Failed to parse NOTIFY message body"); soup_server_message_set_status ( msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, "Unable to parse NOTIFY message"); return; } priv = gupnp_service_proxy_get_instance_private (proxy); /* Get root propertyset element */ node = xmlDocGetRootElement (doc); if (node == NULL || strcmp ((char *) node->name, "propertyset") || priv->sid == NULL) { /* Empty or unsupported */ xmlFreeDoc (doc); soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL); return; } /* * Some UPnP stacks (hello, myigd/1.0) block when sending a NOTIFY, so * call the callbacks in an idle handler so that if the client calls the * device in the notify callback the server can actually respond. */ emit_notify_data = emit_notify_data_new (hdr, seq, doc); g_queue_push_tail (priv->pending_notifies, emit_notify_data); if (!priv->notify_idle_src) { priv->notify_idle_src = g_idle_source_new(); g_source_set_callback (priv->notify_idle_src, emit_notifications, proxy, NULL); g_source_attach (priv->notify_idle_src, g_main_context_get_thread_default ()); g_source_unref (priv->notify_idle_src); } /* Everything went OK */ soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL); } /* * Generates a timeout header for the subscription timeout specified * in our GUPnPContext. */ static char * make_timeout_header (GUPnPContext *context) { guint timeout; timeout = gupnp_context_get_subscription_timeout (context); if (timeout > 0) return g_strdup_printf ("Second-%d", timeout); else return g_strdup ("infinite"); } typedef struct { GUPnPServiceProxy *proxy; SoupMessage *msg; } SubscriptionCallData; /* * Subscription expired. */ static gboolean subscription_expire (gpointer user_data) { GUPnPServiceProxy *proxy; GUPnPServiceProxyPrivate *priv; GUPnPContext *context; SoupMessage *msg; SoupSession *session; char *sub_url, *timeout, *local_sub_url; proxy = GUPNP_SERVICE_PROXY (user_data); priv = gupnp_service_proxy_get_instance_private (proxy); /* Reset timeout ID */ priv->subscription_timeout_src = NULL; g_return_val_if_fail (priv->sid != NULL, FALSE); /* Send renewal message */ context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); /* Create subscription message */ sub_url = gupnp_service_info_get_event_subscription_url (GUPNP_SERVICE_INFO (proxy)); local_sub_url = gupnp_context_rewrite_uri (context, sub_url); g_free (sub_url); msg = soup_message_new (GENA_METHOD_SUBSCRIBE, local_sub_url); g_free (local_sub_url); g_return_val_if_fail (msg != NULL, FALSE); SoupMessageHeaders *request_headers = soup_message_get_request_headers (msg); /* Add headers */ soup_message_headers_append (request_headers, "SID", priv->sid); timeout = make_timeout_header (context); soup_message_headers_append (request_headers, "Timeout", timeout); g_free (timeout); /* And send it off */ session = gupnp_context_get_session (context); SubscriptionCallData *data = g_new0 (SubscriptionCallData, 1); data->msg = msg; data->proxy = proxy; soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, priv->pending_messages, subscribe_got_response, data); return FALSE; } /* * Received subscription response. */ static void subscribe_got_response (GObject *source, GAsyncResult *res, gpointer user_data) { GError *error = NULL; GUPnPServiceProxyPrivate *priv; SubscriptionCallData *data = user_data; priv = gupnp_service_proxy_get_instance_private (data->proxy); GInputStream *is = soup_session_send_finish (SOUP_SESSION (source), res, &error); /* Cancelled? */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { goto out; } else if (error != NULL) { // FIXME: Is just propagating the error the right way? goto hdr_err; } // We don't need the body, it should be empty anyway g_input_stream_close (is, NULL, NULL); g_object_unref (is); /* Remove subscription timeout */ g_clear_pointer (&priv->subscription_timeout_src, g_source_destroy); /* Check whether the subscription is still wanted */ if (!priv->subscribed) { goto out; } /* Reset SID */ g_clear_pointer (&priv->sid, g_free); SoupStatus status = soup_message_get_status (data->msg); SoupMessageHeaders *response_headers = soup_message_get_response_headers (data->msg); /* Check message status */ if (SOUP_STATUS_IS_SUCCESSFUL (status)) { /* Success. */ const char *hdr; int timeout; /* Save SID. */ hdr = soup_message_headers_get_one (response_headers, "SID"); if (hdr == NULL) { error = g_error_new (GUPNP_EVENTING_ERROR, GUPNP_EVENTING_ERROR_SUBSCRIPTION_LOST, "No SID in SUBSCRIBE response"); goto hdr_err; } priv->sid = g_strdup (hdr); /* Figure out when the subscription times out */ hdr = soup_message_headers_get_one (response_headers, "Timeout"); if (hdr == NULL) { g_warning ("No Timeout in SUBSCRIBE response."); goto out; } if (strncmp (hdr, "Second-", strlen ("Second-")) == 0) { /* We have a finite timeout */ timeout = atoi (hdr + strlen ("Second-")); if (timeout < 0) { g_warning ("Invalid time-out specified. " "Assuming default value of %d.", GENA_DEFAULT_TIMEOUT); timeout = GENA_DEFAULT_TIMEOUT; } /* We want to resubscribe before the subscription * expires. We do that somewhat around the middle of the * subscription period and introduce some random jitter * around that, so we do not bombard the services with * all the re-subscription all at once */ int jitter = g_random_int_range (-timeout / 4, timeout / 4); timeout = timeout / 2 + jitter; g_debug ("Service announced timeout of %s, will " "re-subscribe in %d seconds", hdr, timeout); /* Add actual timeout */ priv->subscription_timeout_src = g_timeout_source_new_seconds (timeout); g_source_set_callback (priv->subscription_timeout_src, subscription_expire, data->proxy, NULL); g_source_attach (priv->subscription_timeout_src, g_main_context_get_thread_default ()); g_source_unref (priv->subscription_timeout_src); } } else { GUPnPContext *context; SoupServer *server; /* Subscription failed. */ error = g_error_new_literal ( GUPNP_EVENTING_ERROR, GUPNP_EVENTING_ERROR_SUBSCRIPTION_FAILED, soup_message_get_reason_phrase (data->msg)); hdr_err: /* Remove listener */ context = gupnp_service_info_get_context ( GUPNP_SERVICE_INFO (data->proxy)); server = gupnp_context_get_server (context); soup_server_remove_handler (server, priv->path); priv->subscribed = FALSE; g_object_notify (G_OBJECT (data->proxy), "subscribed"); /* Emit subscription-lost */ g_signal_emit (data->proxy, signals[SUBSCRIPTION_LOST], 0, error); g_error_free (error); } out: g_object_unref (data->msg); g_free (user_data); } /* * Subscribe to this service. */ static void subscribe (GUPnPServiceProxy *proxy) { GUPnPContext *context; GUPnPServiceProxyPrivate *priv; SoupMessage *msg; SoupSession *session; SoupServer *server; GUri *uri; char *uri_string; char *sub_url, *delivery_url, *timeout; /* Remove subscription timeout */ priv = gupnp_service_proxy_get_instance_private (proxy); if (priv->subscription_timeout_src) { g_source_destroy (priv->subscription_timeout_src); priv->subscription_timeout_src = NULL; } context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); /* Create subscription message */ sub_url = gupnp_service_info_get_event_subscription_url (GUPNP_SERVICE_INFO (proxy)); msg = NULL; if (sub_url != NULL) { char *local_sub_url = NULL; local_sub_url = gupnp_context_rewrite_uri (context, sub_url); g_free (sub_url); msg = soup_message_new (GENA_METHOD_SUBSCRIBE, local_sub_url); g_free (local_sub_url); } if (msg == NULL) { GError *error; /* Subscription failed. */ priv->subscribed = FALSE; g_object_notify (G_OBJECT (proxy), "subscribed"); /* Emit subscription-lost */ error = g_error_new (GUPNP_SERVER_ERROR, GUPNP_SERVER_ERROR_INVALID_URL, "No valid subscription URL defined"); g_signal_emit (proxy, signals[SUBSCRIPTION_LOST], 0, error); g_error_free (error); return; } /* Add headers */ uri = _gupnp_context_get_server_uri (context); GUri *new_uri = soup_uri_copy (uri, SOUP_URI_PATH, priv->path, NULL); uri_string = g_uri_to_string_partial (new_uri, G_URI_HIDE_PASSWORD); g_uri_unref (new_uri); g_uri_unref (uri); delivery_url = g_strdup_printf ("<%s>", uri_string); g_free (uri_string); SoupMessageHeaders *request_headers = soup_message_get_request_headers (msg); soup_message_headers_append (request_headers, "Callback", delivery_url); g_free (delivery_url); soup_message_headers_append (request_headers, "NT", "upnp:event"); timeout = make_timeout_header (context); soup_message_headers_append (request_headers, "Timeout", timeout); g_free (timeout); /* Listen for events */ server = gupnp_context_get_server (context); soup_server_add_handler (server, priv->path, server_handler, proxy, NULL); /* And send our subscription message off */ session = gupnp_context_get_session (context); SubscriptionCallData *data = g_new0 (SubscriptionCallData, 1); data->msg = msg; data->proxy = proxy; soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, priv->pending_messages, subscribe_got_response, data); } /* * Unsubscribe from this service. */ static void unsubscribe (GUPnPServiceProxy *proxy) { GUPnPContext *context; GUPnPServiceProxyPrivate *priv; SoupSession *session; SoupServer *server; context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); priv = gupnp_service_proxy_get_instance_private (proxy); /* Remove server handler */ server = gupnp_context_get_server (context); soup_server_remove_handler (server, priv->path); if (priv->sid != NULL) { SoupMessage *msg; char *sub_url, *local_sub_url; /* Create unsubscription message */ sub_url = gupnp_service_info_get_event_subscription_url (GUPNP_SERVICE_INFO (proxy)); local_sub_url = gupnp_context_rewrite_uri (context, sub_url); g_free (sub_url); msg = soup_message_new (GENA_METHOD_UNSUBSCRIBE, local_sub_url); g_free (local_sub_url); if (msg != NULL) { /* Add headers */ soup_message_headers_append ( soup_message_get_request_headers (msg), "SID", priv->sid); /* And queue it */ session = gupnp_context_get_session (context); // We do not care about the result of that message soup_session_send_async ( session, msg, G_PRIORITY_DEFAULT, NULL, NULL, NULL); g_object_unref (msg); } /* Reset SID */ g_free (priv->sid); priv->sid = NULL; /* Reset sequence number */ priv->seq = 0; } /* Remove subscription timeout */ if (priv->subscription_timeout_src) { g_source_destroy (priv->subscription_timeout_src); priv->subscription_timeout_src = NULL; } } /** * gupnp_service_proxy_set_subscribed: * @proxy: A #GUPnPServiceProxy * @subscribed: %TRUE to subscribe to this service * * (Un)subscribes to this service. * * The relevant messages are not immediately sent but queued. * If you want to unsubcribe from this service because the application * is quitting, rely on automatic synchronised unsubscription on object * destruction instead. **/ void gupnp_service_proxy_set_subscribed (GUPnPServiceProxy *proxy, gboolean subscribed) { GUPnPServiceProxyPrivate *priv; g_return_if_fail (GUPNP_IS_SERVICE_PROXY (proxy)); priv = gupnp_service_proxy_get_instance_private (proxy); if (priv->subscribed == subscribed) return; priv->subscribed = subscribed; if (subscribed) subscribe (proxy); else unsubscribe (proxy); g_object_notify (G_OBJECT (proxy), "subscribed"); } /** * gupnp_service_proxy_get_subscribed: * @proxy: A #GUPnPServiceProxy * * Returns if we are subscribed to this service. * * Return value: %TRUE if we are subscribed to this service, otherwise %FALSE. **/ gboolean gupnp_service_proxy_get_subscribed (GUPnPServiceProxy *proxy) { GUPnPServiceProxyPrivate *priv; g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE); priv = gupnp_service_proxy_get_instance_private (proxy); return priv->subscribed; } /** * gupnp_service_proxy_call_action_async: * @proxy: (transfer none): A #GUPnPServiceProxy * @action: (transfer none): A #GUPnPServiceProxyAction to call * @cancellable: (allow-none): A #GCancellable which can be used to cancel the * current action call * @callback: (scope async): A #GAsyncReadyCallback to call when the action is * finished. * @user_data: (closure): User data for @callback * * Start a call on the remote UPnP service using the pre-configured @action. * Use gupnp_service_proxy_call_action_finish() in the @callback to finalize * the call and gupnp_service_proxy_action_get_result(), * gupnp_service_proxy_action_get_result_hash() or * gupnp_service_proxy_action_get_result_list() to extract the result of the * remote call. * Since: 1.2.0 */ void gupnp_service_proxy_call_action_async (GUPnPServiceProxy *proxy, GUPnPServiceProxyAction *action, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; GError *error = NULL; g_return_if_fail (GUPNP_IS_SERVICE_PROXY (proxy)); task = g_task_new (proxy, cancellable, callback, user_data); char *task_name = g_strdup_printf ("UPnP Call \"%s\"", action->name); g_task_set_name (task, task_name); g_free (task_name); g_task_set_task_data ( task, gupnp_service_proxy_action_ref (action), (GDestroyNotify) gupnp_service_proxy_action_unref); prepare_action_msg (proxy, action, SOUP_METHOD_POST, &error); if (error != NULL) { g_task_return_error (task, error); g_object_unref (task); } else { gupnp_service_proxy_action_queue_task (task); } } /** * gupnp_service_proxy_call_action_finish: * @proxy: a #GUPnPServiceProxy * @result: a #GAsyncResult * @error: (inout)(optional)(nullable): Return location for a #GError, or %NULL * * Finish an asynchronous call initiated with * gupnp_service_proxy_call_action_async(). * * Returns: (nullable) (transfer none): %NULL, if the call had an error, the action otherwise. * Since: 1.2.0 */ GUPnPServiceProxyAction * gupnp_service_proxy_call_action_finish (GUPnPServiceProxy *proxy, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (G_TASK (result), proxy), NULL); GUPnPServiceProxyAction *action = g_task_get_task_data (G_TASK (result)); g_clear_weak_pointer (&action->proxy); action->pending = FALSE; return g_task_propagate_pointer (G_TASK (result), error); } /** * gupnp_service_proxy_call_action: * @proxy: (transfer none): A #GUPnPServiceProxy * @action: (transfer none): An action * @cancellable: (allow-none): A #GCancellable which can be used to cancel the * current action call * @error: (inout)(optional)(nullable): Return location for a #GError, or %NULL. * * Synchronously call the @action on the remote UPnP service. * * Returns: (nullable)(transfer none): %NULL on error, @action if successful. * Since: 1.2.0 */ GUPnPServiceProxyAction * gupnp_service_proxy_call_action (GUPnPServiceProxy *proxy, GUPnPServiceProxyAction *action, GCancellable *cancellable, GError **error) { GUPnPContext *context = NULL; SoupSession *session = NULL; g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL); g_return_val_if_fail (!action->pending, NULL); if (!prepare_action_msg (proxy, action, SOUP_METHOD_POST, error)) { return NULL; } context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy)); session = gupnp_context_get_session (context); action->response = soup_session_send_and_read (session, action->msg, cancellable, &action->error); if (action->error != NULL) { goto out; } /* If not allowed, try again */ if (soup_message_get_status (action->msg) == SOUP_STATUS_METHOD_NOT_ALLOWED) { if (prepare_action_msg (proxy, action, "M-POST", &action->error)) { g_bytes_unref (action->response); action->response = soup_session_send_and_read (session, action->msg, cancellable, &action->error); } } if (action->error == NULL) gupnp_service_proxy_action_check_response (action); out: if (action->error != NULL) { g_propagate_error (error, g_error_copy (action->error)); return NULL; } return action; } /** * gupnp_service_proxy_set_credentials: * @proxy: A #GUPnPServiceProxy * @user: user name for authentication * @password: user password for authentication * * Sets user and password for authentication * * Since: 1.6.4 **/ void gupnp_service_proxy_set_credentials (GUPnPServiceProxy *proxy, const char *user, const char *password) { GUPnPServiceProxyPrivate *priv; priv = gupnp_service_proxy_get_instance_private (proxy); g_clear_pointer (&priv->user, g_free); g_clear_pointer (&priv->password, g_free); priv->user = g_strdup (user); priv->password = g_strdup (password); }