/* XML console plugin * * Copyright © 2011 Collabora Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #include "console.h" #include #include #include #include "extensions/extensions.h" #include /************************* * Plugin implementation * *************************/ static guint debug = 0; #define DEBUG(format, ...) \ G_STMT_START { \ if (debug != 0) \ g_debug ("%s: " format, G_STRFUNC, ## __VA_ARGS__); \ } G_STMT_END static const GDebugKey debug_keys[] = { { "console", 1 }, { NULL, 0 } }; static void plugin_iface_init ( gpointer g_iface, gpointer data); static const gchar * const sidecar_interfaces[] = { GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, NULL }; G_DEFINE_TYPE_WITH_CODE (GabbleConsolePlugin, gabble_console_plugin, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GABBLE_TYPE_PLUGIN, plugin_iface_init); ) static void gabble_console_plugin_init (GabbleConsolePlugin *self) { } static void gabble_console_plugin_class_init (GabbleConsolePluginClass *klass) { } static void gabble_console_plugin_create_sidecar_async ( GabblePlugin *plugin, const gchar *sidecar_interface, GabblePluginConnection *connection, WockySession *session, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *result = g_simple_async_result_new (G_OBJECT (plugin), callback, user_data, gabble_console_plugin_create_sidecar_async); GabbleSidecar *sidecar = NULL; if (!tp_strdiff (sidecar_interface, GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE)) { sidecar = g_object_new (GABBLE_TYPE_CONSOLE_SIDECAR, "connection", connection, "session", session, NULL); } else { g_simple_async_result_set_error (result, TP_ERROR, TP_ERROR_NOT_IMPLEMENTED, "'%s' not implemented", sidecar_interface); } if (sidecar != NULL) g_simple_async_result_set_op_res_gpointer (result, sidecar, g_object_unref); g_simple_async_result_complete_in_idle (result); g_object_unref (result); } static GabbleSidecar * gabble_console_plugin_create_sidecar_finish ( GabblePlugin *plugin, GAsyncResult *result, GError **error) { GabbleSidecar *sidecar; if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error)) return NULL; g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (plugin), gabble_console_plugin_create_sidecar_async), NULL); sidecar = GABBLE_SIDECAR (g_simple_async_result_get_op_res_gpointer ( G_SIMPLE_ASYNC_RESULT (result))); return g_object_ref (sidecar); } static void plugin_iface_init ( gpointer g_iface, gpointer data G_GNUC_UNUSED) { GabblePluginInterface *iface = g_iface; iface->name = "XMPP console"; iface->version = PACKAGE_VERSION; iface->sidecar_interfaces = sidecar_interfaces; iface->create_sidecar_async = gabble_console_plugin_create_sidecar_async; iface->create_sidecar_finish = gabble_console_plugin_create_sidecar_finish; } GabblePlugin * gabble_plugin_create (void) { debug = g_parse_debug_string (g_getenv ("GABBLE_DEBUG"), debug_keys, G_N_ELEMENTS (debug_keys) - 1); DEBUG ("loaded"); return g_object_new (GABBLE_TYPE_CONSOLE_PLUGIN, NULL); } /************************** * Sidecar implementation * **************************/ enum { PROP_0, PROP_CONNECTION, PROP_SESSION, PROP_SPEW }; struct _GabbleConsoleSidecarPrivate { WockySession *session; TpBaseConnection *connection; WockyXmppReader *reader; WockyXmppWriter *writer; /* %TRUE if we should emit signals when sending or receiving stanzas */ gboolean spew; /* 0 if spew is FALSE; or a WockyPorter handler id for all incoming stanzas * if spew is TRUE. */ guint incoming_handler; /* 0 if spew is FALSE; a GLib signal handler id for WockyPorter::sending if * spew is TRUE. */ gulong sending_id; }; static void sidecar_iface_init ( gpointer g_iface, gpointer data); static void console_iface_init ( gpointer g_iface, gpointer data); static void gabble_console_sidecar_set_spew ( GabbleConsoleSidecar *self, gboolean spew); G_DEFINE_TYPE_WITH_CODE (GabbleConsoleSidecar, gabble_console_sidecar, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SIDECAR, sidecar_iface_init); G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_GABBLE_PLUGIN_CONSOLE, console_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, tp_dbus_properties_mixin_iface_init); ) static void gabble_console_sidecar_init (GabbleConsoleSidecar *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GABBLE_TYPE_CONSOLE_SIDECAR, GabbleConsoleSidecarPrivate); self->priv->reader = wocky_xmpp_reader_new_no_stream_ns ( WOCKY_XMPP_NS_JABBER_CLIENT); self->priv->writer = wocky_xmpp_writer_new_no_stream (); } static void gabble_console_sidecar_get_property ( GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); switch (property_id) { case PROP_SPEW: g_value_set_boolean (value, self->priv->spew); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } } static void gabble_console_sidecar_set_property ( GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); switch (property_id) { case PROP_CONNECTION: g_assert (self->priv->connection == NULL); /* construct-only */ self->priv->connection = g_value_dup_object (value); break; case PROP_SESSION: g_assert (self->priv->session == NULL); /* construct-only */ self->priv->session = g_value_dup_object (value); break; case PROP_SPEW: gabble_console_sidecar_set_spew (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } } static void gabble_console_sidecar_dispose (GObject *object) { void (*chain_up) (GObject *) = G_OBJECT_CLASS (gabble_console_sidecar_parent_class)->dispose; GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); gabble_console_sidecar_set_spew (self, FALSE); tp_clear_object (&self->priv->connection); tp_clear_object (&self->priv->reader); tp_clear_object (&self->priv->writer); tp_clear_object (&self->priv->session); if (chain_up != NULL) chain_up (object); } static void gabble_console_sidecar_class_init (GabbleConsoleSidecarClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); static TpDBusPropertiesMixinPropImpl console_props[] = { { "SpewStanzas", "spew-stanzas", "spew-stanzas" }, { NULL }, }; static TpDBusPropertiesMixinIfaceImpl interfaces[] = { { GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, tp_dbus_properties_mixin_getter_gobject_properties, /* FIXME: if we were feeling clever, we'd override the setter so that * we can monitor the bus name of any application which sets * SpewStanzas to TRUE and flip it back to false when that application * dies. * * Alternatively, we could just replace this sidecar with a channel. */ tp_dbus_properties_mixin_setter_gobject_properties, console_props }, { NULL }, }; object_class->get_property = gabble_console_sidecar_get_property; object_class->set_property = gabble_console_sidecar_set_property; object_class->dispose = gabble_console_sidecar_dispose; g_type_class_add_private (klass, sizeof (GabbleConsoleSidecarPrivate)); g_object_class_install_property (object_class, PROP_CONNECTION, g_param_spec_object ("connection", "Connection", "Gabble connection", GABBLE_TYPE_PLUGIN_CONNECTION, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_SESSION, g_param_spec_object ("session", "Session", "Wocky session", WOCKY_TYPE_SESSION, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_SPEW, g_param_spec_boolean ("spew-stanzas", "SpewStanzas", "If %TRUE, someone wants us to spit out a tonne of stanzas", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); klass->props_class.interfaces = interfaces; tp_dbus_properties_mixin_class_init (object_class, G_STRUCT_OFFSET (GabbleConsoleSidecarClass, props_class)); } static void sidecar_iface_init ( gpointer g_iface, gpointer data) { GabbleSidecarInterface *iface = g_iface; iface->interface = GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE; iface->get_immutable_properties = NULL; } static gboolean incoming_cb ( WockyPorter *porter, WockyStanza *stanza, gpointer user_data) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (user_data); const guint8 *body; gsize length; wocky_xmpp_writer_write_stanza (self->priv->writer, stanza, &body, &length); gabble_svc_gabble_plugin_console_emit_stanza_received (self, (const gchar *) body); return FALSE; } static void sending_cb ( WockyPorter *porter, WockyStanza *stanza, gpointer user_data) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (user_data); if (stanza != NULL) { const guint8 *body; gsize length; wocky_xmpp_writer_write_stanza (self->priv->writer, stanza, &body, &length); gabble_svc_gabble_plugin_console_emit_stanza_sent (self, (const gchar *) body); } } static void gabble_console_sidecar_set_spew ( GabbleConsoleSidecar *self, gboolean spew) { GabbleConsoleSidecarPrivate *priv = self->priv; if (!spew != !priv->spew) { WockyPorter *porter = wocky_session_get_porter (self->priv->session); const gchar *props[] = { "SpewStanzas", NULL }; priv->spew = spew; tp_dbus_properties_mixin_emit_properties_changed (G_OBJECT (self), GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, props); if (spew) { g_return_if_fail (priv->incoming_handler == 0); priv->incoming_handler = wocky_porter_register_handler_from_anyone ( porter, WOCKY_STANZA_TYPE_NONE, WOCKY_STANZA_SUB_TYPE_NONE, WOCKY_PORTER_HANDLER_PRIORITY_MAX, incoming_cb, self, NULL); g_return_if_fail (priv->sending_id == 0); priv->sending_id = g_signal_connect (porter, "sending", (GCallback) sending_cb, self); } else { g_return_if_fail (priv->incoming_handler != 0); wocky_porter_unregister_handler (porter, priv->incoming_handler); priv->incoming_handler = 0; g_return_if_fail (priv->sending_id != 0); g_signal_handler_disconnect (porter, priv->sending_id); priv->sending_id = 0; } } } static void return_from_send_iq ( GObject *source, GAsyncResult *result, gpointer user_data) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (source); DBusGMethodInvocation *context = user_data; GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); GError *error = NULL; if (g_simple_async_result_propagate_error (simple, &error)) { dbus_g_method_return_error (context, error); g_error_free (error); } else { WockyStanza *reply = g_simple_async_result_get_op_res_gpointer (simple); WockyStanzaSubType sub_type; const guint8 *body; gsize length; wocky_stanza_get_type_info (reply, NULL, &sub_type); wocky_xmpp_writer_write_stanza (self->priv->writer, reply, &body, &length); /* woop woop */ gabble_svc_gabble_plugin_console_return_from_send_iq (context, sub_type == WOCKY_STANZA_SUB_TYPE_RESULT ? "result" : "error", (const gchar *) body); } } static void console_iq_reply_cb ( GObject *source, GAsyncResult *result, gpointer user_data) { WockyPorter *porter = WOCKY_PORTER (source); GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (user_data); GError *error = NULL; WockyStanza *reply = wocky_porter_send_iq_finish (porter, result, &error); if (reply != NULL) { g_simple_async_result_set_op_res_gpointer (simple, reply, g_object_unref); } else { g_simple_async_result_set_from_error (simple, error); g_error_free (error); } g_simple_async_result_complete (simple); g_object_unref (simple); } static gboolean get_iq_type (const gchar *type_str, WockyStanzaSubType *sub_type_out, GError **error) { if (!wocky_strdiff (type_str, "get")) { *sub_type_out = WOCKY_STANZA_SUB_TYPE_GET; return TRUE; } if (!wocky_strdiff (type_str, "set")) { *sub_type_out = WOCKY_STANZA_SUB_TYPE_SET; return TRUE; } g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, "Type must be 'get' or 'set', not '%s'", type_str); return FALSE; } static gboolean validate_jid (const gchar **to, GError **error) { if (tp_str_empty (*to)) { *to = NULL; return TRUE; } if (wocky_decode_jid (*to, NULL, NULL, NULL)) return TRUE; g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, "'%s' is not a valid (or empty) JID", *to); return FALSE; } /* * @xml: doesn't actually have to be a top-level stanza. It can be the body of * an IQ or whatever. If it has no namespace, it's assumed to be in * jabber:client. */ static gboolean parse_me_a_stanza ( GabbleConsoleSidecar *self, const gchar *xml, WockyStanza **stanza_out, GError **error) { GabbleConsoleSidecarPrivate *priv = self->priv; WockyStanza *stanza; wocky_xmpp_reader_reset (priv->reader); wocky_xmpp_reader_push (priv->reader, (const guint8 *) xml, strlen (xml)); *error = wocky_xmpp_reader_get_error (priv->reader); if (*error != NULL) return FALSE; stanza = wocky_xmpp_reader_pop_stanza (priv->reader); if (stanza == NULL) { g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, "Incomplete stanza! Bad person."); return FALSE; } *stanza_out = stanza; return TRUE; } static void console_send_iq ( GabbleSvcGabblePluginConsole *sidecar, const gchar *type_str, const gchar *to, const gchar *body, DBusGMethodInvocation *context) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (sidecar); WockyPorter *porter = wocky_session_get_porter (self->priv->session); WockyStanzaSubType sub_type; WockyStanza *fragment; GError *error = NULL; if (get_iq_type (type_str, &sub_type, &error) && validate_jid (&to, &error) && parse_me_a_stanza (self, body, &fragment, &error)) { GSimpleAsyncResult *simple = g_simple_async_result_new (G_OBJECT (self), return_from_send_iq, context, console_send_iq); WockyStanza *stanza = wocky_stanza_build (WOCKY_STANZA_TYPE_IQ, sub_type, NULL, to, NULL); wocky_node_add_node_tree (wocky_stanza_get_top_node (stanza), WOCKY_NODE_TREE (fragment)); wocky_porter_send_iq_async (porter, stanza, NULL, console_iq_reply_cb, simple); g_object_unref (fragment); } else { DEBUG ("%s", error->message); dbus_g_method_return_error (context, error); g_error_free (error); } } static void console_stanza_sent_cb ( GObject *source, GAsyncResult *result, gpointer user_data) { WockyPorter *porter = WOCKY_PORTER (source); DBusGMethodInvocation *context = user_data; GError *error = NULL; if (wocky_porter_send_finish (porter, result, &error)) { gabble_svc_gabble_plugin_console_return_from_send_stanza (context); } else { dbus_g_method_return_error (context, error); g_clear_error (&error); } } static gboolean stanza_looks_coherent ( WockyStanza *stanza, GError **error) { WockyNode *top_node = wocky_stanza_get_top_node (stanza); WockyStanzaType t; WockyStanzaSubType st; wocky_stanza_get_type_info (stanza, &t, &st); if (t == WOCKY_STANZA_TYPE_UNKNOWN) { g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, "I don't know what a <%s xmlns='%s'/> is", top_node->name, g_quark_to_string (top_node->ns)); return FALSE; } else if (st == WOCKY_STANZA_SUB_TYPE_UNKNOWN) { g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, "I don't know what type='%s' means", wocky_node_get_attribute (top_node, "type")); return FALSE; } return TRUE; } static void console_send_stanza ( GabbleSvcGabblePluginConsole *sidecar, const gchar *xml, DBusGMethodInvocation *context) { GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (sidecar); WockyPorter *porter = wocky_session_get_porter (self->priv->session); WockyStanza *stanza = NULL; GError *error = NULL; if (parse_me_a_stanza (self, xml, &stanza, &error) && stanza_looks_coherent (stanza, &error)) { wocky_porter_send_async (porter, stanza, NULL, console_stanza_sent_cb, context); } else { DEBUG ("%s", error->message); dbus_g_method_return_error (context, error); g_error_free (error); } tp_clear_object (&stanza); } static void console_iface_init ( gpointer klass, gpointer data G_GNUC_UNUSED) { #define IMPLEMENT(x) gabble_svc_gabble_plugin_console_implement_##x (\ klass, console_##x) IMPLEMENT (send_iq); IMPLEMENT (send_stanza); #undef IMPLEMENT }