/* -*- Mode: C++ -*- * Worldvisions Weaver Software: * Copyright (C) 2005-2006 Net Integration Technologies, Inc. * * Pathfinder Software: * Copyright (C) 2007, Carillon Information Security Inc. * * This library is licensed under the LGPL, please read LICENSE for details. * */ #include "wvdbusserver.h" #include "wvdbusconn.h" #include "wvstrutils.h" #include "wvuid.h" #include "wvtcplistener.h" #include "wvdelayedcallback.h" #undef interface // windows #include #include "wvx509.h" class WvDBusServerAuth : public IWvDBusAuth { enum State { NullWait, AuthWait, BeginWait }; State state; wvuid_t client_uid; public: WvDBusServerAuth(); virtual bool authorize(WvDBusConn &c); virtual wvuid_t get_uid() { return client_uid; } }; WvDBusServerAuth::WvDBusServerAuth() { state = NullWait; client_uid = WVUID_INVALID; } bool WvDBusServerAuth::authorize(WvDBusConn &c) { c.log("State=%s\n", state); if (state == NullWait) { char buf[1]; size_t len = c.read(buf, 1); if (len == 1 && buf[0] == '\0') { state = AuthWait; // fall through } else if (len > 0) c.seterr("Client didn't start with NUL byte"); else return false; // no data yet, come back later } const char *line = c.in(); if (!line) return false; // not done yet WvStringList words; words.split(line); WvString cmd(words.popstr()); if (state == AuthWait) { if (!strcasecmp(cmd, "AUTH")) { // FIXME actually check authentication information! WvString typ(words.popstr()); if (!strcasecmp(typ, "EXTERNAL")) { WvString uid = WvHexDecoder().strflushstr(words.popstr()); if (!!uid) { // FIXME: Check that client is on the same machine! client_uid = uid.num(); } state = BeginWait; c.out("OK f00f\r\n"); } else { // Some clients insist that we reject something because // their state machine can't handle us accepting just the // "AUTH " command. c.out("REJECTED EXTERNAL\r\n"); // no change in state } } else c.seterr("AUTH command expected: '%s'", line); } else if (state == BeginWait) { if (!strcasecmp(cmd, "BEGIN")) return true; // done else c.seterr("BEGIN command expected: '%s'", line); } return false; } WvDBusServer::WvDBusServer() : log("DBus Server", WvLog::Debug) { // user must now call listen() at least once. add(&listeners, false, "listeners"); } WvDBusServer::~WvDBusServer() { close(); zap(); } void WvDBusServer::listen(WvStringParm moniker) { IWvListener *listener = IWvListener::create(moniker); log(WvLog::Info, "Listening on '%s'\n", *listener->src()); if (!listener->isok()) log(WvLog::Info, "Can't listen: %s\n", listener->errstr()); listener->onaccept(wv::bind(&WvDBusServer::new_connection_cb, this, _1)); listeners.add(listener, true, "listener"); } bool WvDBusServer::isok() const { if (geterr()) return false; WvIStreamList::Iter i(listeners); for (i.rewind(); i.next(); ) if (!i->isok()) return false; return WvIStreamList::isok(); } int WvDBusServer::geterr() const { return WvIStreamList::geterr(); } WvString WvDBusServer::get_addr() { // FIXME assumes tcp WvIStreamList::Iter i(listeners); for (i.rewind(); i.next(); ) if (i->isok()) return WvString("tcp:%s", *i->src()); return WvString(); } void WvDBusServer::register_name(WvStringParm name, WvDBusConn *conn) { name_to_conn[name] = conn; } void WvDBusServer::unregister_name(WvStringParm name, WvDBusConn *conn) { assert(name_to_conn[name] == conn); name_to_conn.erase(name); } void WvDBusServer::unregister_conn(WvDBusConn *conn) { { std::map::iterator i; for (i = name_to_conn.begin(); i != name_to_conn.end(); ) { if (i->second == conn) { name_to_conn.erase(i->first); i = name_to_conn.begin(); } else ++i; } } all_conns.unlink(conn); } bool WvDBusServer::do_server_msg(WvDBusConn &conn, WvDBusMsg &msg) { WvString method(msg.get_member()); if (msg.get_path() == "/org/freedesktop/DBus/Local") { if (method == "Disconnected") return true; // nothing to do until their *stream* disconnects } if (msg.get_dest() != "org.freedesktop.DBus") return false; // dbus-daemon seems to ignore the path as long as the service is right //if (msg.get_path() != "/org/freedesktop/DBus") return false; // I guess it's for us! if (method == "Hello") { log("hello_cb\n"); msg.reply().append(conn.uniquename()).send(conn); return true; } else if (method == "RequestName") { WvDBusMsg::Iter args(msg); WvString _name = args.getnext(); // uint32_t flags = args.getnext(); // supplied, but ignored log("request_name_cb(%s)\n", _name); register_name(_name, &conn); msg.reply().append((uint32_t)DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) .send(conn); return true; } else if (method == "ReleaseName") { WvDBusMsg::Iter args(msg); WvString _name = args.getnext(); log("release_name_cb(%s)\n", _name); unregister_name(_name, &conn); msg.reply().append((uint32_t)DBUS_RELEASE_NAME_REPLY_RELEASED) .send(conn); return true; } else if (method == "NameHasOwner") { WvDBusMsg::Iter args(msg); WvString known_name = args.getnext(); WvDBusConn *serv = name_to_conn[known_name]; msg.reply().append(!!serv).send(conn); return true; } else if (method == "GetNameOwner") { WvDBusMsg::Iter args(msg); WvString known_name = args.getnext(); WvDBusConn *serv = name_to_conn[known_name]; if (serv) msg.reply().append(serv->uniquename()).send(conn); else WvDBusError(msg, "org.freedesktop.DBus.Error.NameHasNoOwner", "No match for name '%s'", known_name).send(conn); return true; } else if (method == "AddMatch") { // we just proxy every signal to everyone for now msg.reply().send(conn); return true; } else if (method == "StartServiceByName") { // we don't actually support this, but returning an error message // confuses perl's Net::DBus library, at least. msg.reply().send(conn); return true; } else if (method == "GetConnectionUnixUser" || method == "GetConnectionUnixUserName") { WvDBusMsg::Iter args(msg); WvString _name = args.getnext(); WvDBusConn *target = name_to_conn[_name]; if (!target) { WvDBusError(msg, "org.freedesktop.DBus.Error.Failed", "No connection found for name '%s'.", _name).send(conn); return true; } wvuid_t client_uid = target->get_uid(); if (client_uid == WVUID_INVALID) { WvDBusError(msg, "org.freedesktop.DBus.Error.Failed", "No user associated with connection '%s'.", target->uniquename()).send(conn); return true; } log("Found unix user for '%s', uid is %s.\n", _name, client_uid); if (method == "GetConnectionUnixUser") { WvString s(client_uid); msg.reply().append((uint32_t)atoll(s)).send(conn); return true; } else if (method == "GetConnectionUnixUserName") { WvString username = wv_username_from_uid(client_uid); if (!username) { WvDBusError(msg, "org.freedesktop.DBus.Error.Failed", "No username for uid='%s'", client_uid) .send(conn); return true; } msg.reply().append(username).send(conn); return true; } else assert(false); // should never happen assert(false); } else if (method == "GetConnectionCert" || method == "GetConnectionCertFingerprint") { WvDBusMsg::Iter args(msg); WvString connid = args.getnext(); WvDBusConn *c = name_to_conn[connid]; WvString ret = c ? c->getattr("peercert") : WvString::null; if (ret.isnull()) WvDBusError(msg, "org.freedesktop.DBus.Error.Failed", "Connection %s did not present a certificate", connid).send(conn); else { if (method == "GetConnectionCertFingerprint") { WvX509 tempcert; // We can assume it's valid because our SSL conn authenticated tempcert.decode(WvX509::CertPEM, ret); ret = tempcert.get_fingerprint(); } msg.reply().append(ret).send(conn); } return true; } else { WvDBusError(msg, "org.freedesktop.DBus.Error.UnknownMethod", "Unknown dbus method '%s'", method).send(conn); return true; // but we've handled it, since it belongs to us } } bool WvDBusServer::do_bridge_msg(WvDBusConn &conn, WvDBusMsg &msg) { // if we get here, nobody handled the message internally, so we can try // to proxy it. if (!!msg.get_dest()) // don't handle blank (broadcast) paths here { std::map::iterator i = name_to_conn.find(msg.get_dest()); WvDBusConn *dconn = (i == name_to_conn.end()) ? NULL : i->second; log("Proxying #%s -> %s\n", msg.get_serial(), dconn ? dconn->uniquename() : WvString("(UNKNOWN)")); dbus_message_set_sender(msg, conn.uniquename().cstr()); if (dconn) dconn->send(msg); else { log(WvLog::Warning, "Proxy: no connection for '%s'\n", msg.get_dest()); return false; } return true; } return false; } bool WvDBusServer::do_broadcast_msg(WvDBusConn &conn, WvDBusMsg &msg) { if (!msg.get_dest()) { log("Broadcasting #%s\n", msg.get_serial()); // note: we broadcast messages even back to the connection where // they originated. I'm not sure this is necessarily ideal, but if // you don't do that then an app can't signal objects that might be // inside itself. WvDBusConnList::Iter i(all_conns); for (i.rewind(); i.next(); ) i->send(msg); return true; } return false; } bool WvDBusServer::do_gaveup_msg(WvDBusConn &conn, WvDBusMsg &msg) { WvDBusError(msg, "org.freedesktop.DBus.Error.NameHasNoOwner", "No running service named '%s'", msg.get_dest()).send(conn); return true; } void WvDBusServer::conn_closed(WvStream &s) { WvDBusConn *c = (WvDBusConn *)&s; unregister_conn(c); this->release(); } void WvDBusServer::new_connection_cb(IWvStream *s) { WvDBusConn *c = new WvDBusConn(s, new WvDBusServerAuth, false); c->addRef(); this->addRef(); all_conns.append(c, true); register_name(c->uniquename(), c); /* The delayed callback here should be explained. The * 'do_broadcast_msg' function sends out data along all connections. * Unfortunately, this is a prime time to figure out a connection died. * A dying connection is removed from the all_conns list... but we are * still in do_broadcast_msg, and using an iterator to go over this list. * The consequences of this were not pleasant, at best. Wrapping cb in a * delayedcallback will always safely remove a connection. */ IWvStreamCallback mycb = wv::bind(&WvDBusServer::conn_closed, this, wv::ref(*c)); c->setclosecallback(wv::delayed(mycb)); c->add_callback(WvDBusConn::PriSystem, wv::bind(&WvDBusServer::do_server_msg, this, wv::ref(*c), _1)); c->add_callback(WvDBusConn::PriBridge, wv::bind(&WvDBusServer::do_bridge_msg, this, wv::ref(*c), _1)); c->add_callback(WvDBusConn::PriBroadcast, wv::bind(&WvDBusServer::do_broadcast_msg, this, wv::ref(*c), _1)); c->add_callback(WvDBusConn::PriGaveUp, wv::bind(&WvDBusServer::do_gaveup_msg, this, wv::ref(*c), _1)); append(c, true, "wvdbus servconn"); }