/* Authors: Pavel Březina Stephen Gallagher Simo Sorce Copyright (C) 2017 Red Hat This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "util/util.h" #include "util/sss_ptr_hash.h" #include "sbus/sbus_private.h" struct sbus_server_on_connection { const char *name; sbus_server_on_connection_cb callback; sbus_server_on_connection_data data; }; static const char * sbus_server_get_filename(const char *address) { const char *filename; filename = strchr(address, '/'); if (filename == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected dbus address [%s].\n", address); return NULL; } return filename; } static const char * sbus_server_get_socket_address(TALLOC_CTX *mem_ctx, const char *address, bool use_symlink) { unsigned long pid; if (!use_symlink) { return talloc_strdup(mem_ctx, address); } pid = getpid(); return talloc_asprintf(mem_ctx, "%s.%lu", address, pid); } static errno_t sbus_server_get_socket(TALLOC_CTX *mem_ctx, const char *address, bool use_symlink, const char **_socket_address, const char **_filename, const char **_symlink) { const char *symlink = NULL; const char *socket_address; const char *filename; /* Get D-Bus socket address. */ socket_address = sbus_server_get_socket_address(mem_ctx, address, use_symlink); if (socket_address == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); return ENOMEM; } /* Get system files names. */ filename = sbus_server_get_filename(socket_address); if (filename == NULL) { return EINVAL; } if (use_symlink) { symlink = sbus_server_get_filename(address); if (symlink == NULL) { return EINVAL; } } if (_socket_address != NULL) { *_socket_address = socket_address; } if (_filename != NULL) { *_filename = filename; } if (_symlink != NULL) { *_symlink = symlink; } return EOK; } static DBusServer * sbus_server_socket_listen(const char *socket_address) { DBusServer *server; DBusError error; char *server_address; dbus_error_init(&error); server = dbus_server_listen(socket_address, &error); if (server == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to start a D-Bus server at " "%s [%s]: %s\n", socket_address, error.name, error.message); } else { server_address = dbus_server_get_address(server); DEBUG(SSSDBG_TRACE_FUNC, "D-BUS Server listening on %s\n", server_address); free(server_address); } dbus_error_free(&error); return server; } static errno_t sbus_server_symlink_create(const char *filename, const char *symlink_filename) { errno_t ret; if (symlink_filename == NULL) { return EINVAL; } DEBUG(SSSDBG_TRACE_LIBS, "Symlinking the dbus path %s to a link %s\n", filename, symlink_filename); errno = 0; ret = symlink(filename, symlink_filename); if (ret != 0 && errno == EEXIST) { /* Perhaps cruft after a previous server? */ errno = 0; ret = unlink(symlink_filename); if (ret != 0) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "Cannot remove old symlink '%s': [%d][%s].\n", symlink_filename, ret, strerror(ret)); return EIO; } errno = 0; ret = symlink(filename, symlink_filename); } if (ret != 0) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "symlink() failed on file '%s': [%d][%s].\n", filename, ret, strerror(ret)); return EIO; } return EOK; } static errno_t sbus_server_symlink_read(const char *name, char *buf, size_t buf_len) { ssize_t num_read = 0; errno_t ret; errno = 0; num_read = readlink(name, buf, buf_len - 1); if (num_read < 0) { ret = errno; DEBUG(SSSDBG_OP_FAILURE, "Unable to read link target [%d]: %s\n", ret, sss_strerror(ret)); return ret; } buf[num_read] = '\0'; return EOK; } static errno_t sbus_server_symlink_pidpath(const char *name, char *buf, size_t buf_len) { int ret; ret = snprintf(buf, buf_len, "%s.%lu", name, (unsigned long)getpid()); if (ret < 0) { DEBUG(SSSDBG_OP_FAILURE, "snprintf failed\n"); return EIO; } else if (ret >= PATH_MAX) { DEBUG(SSSDBG_OP_FAILURE, "path too long?!?!\n"); return EIO; } return EOK; } static void sbus_server_symlink_remove(const char *name) { char target[PATH_MAX]; char pidpath[PATH_MAX]; errno_t ret; ret = sbus_server_symlink_read(name, target, PATH_MAX); if (ret != EOK) { goto done; } DEBUG(SSSDBG_TRACE_ALL, "The symlink points to [%s]\n", target); ret = sbus_server_symlink_pidpath(name, pidpath, PATH_MAX); if (ret != EOK) { goto done; } DEBUG(SSSDBG_TRACE_ALL, "The path including our pid is [%s]\n", pidpath); /* We can only remove the symlink if it points to * a socket with the same PID. */ if (strcmp(pidpath, target) != 0) { DEBUG(SSSDBG_CONF_SETTINGS, "Will not remove symlink, seems to be " "owned by another process\n"); ret = ERR_INTERNAL; goto done; } ret = unlink(name); if (ret != 0) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "unlink failed to remove [%s] [%d]: %s\n", name, ret, sss_strerror(ret)); goto done; } ret = EOK; done: if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Unable to remove symlink [%s]\n", name); return; } DEBUG(SSSDBG_TRACE_FUNC, "Symlink removed [%s]\n", name); } static errno_t sbus_server_check_file(const char *filename, uid_t uid, gid_t gid) { struct stat stat_buf; errno_t ret; /* Both check_file and chmod can handle both the symlink and the socket */ ret = check_file(filename, getuid(), getgid(), S_IFSOCK, S_IFMT, &stat_buf, true); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "check_file failed for [%s].\n", filename); return ret; } if ((stat_buf.st_mode & ~S_IFMT) != (S_IRUSR | S_IWUSR)) { ret = chmod(filename, (S_IRUSR | S_IWUSR)); if (ret != EOK) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "chmod failed for [%s] [%d]: %s\n", filename, ret, sss_strerror(ret)); return ret; } } if (stat_buf.st_uid != uid || stat_buf.st_gid != gid) { ret = chown(filename, uid, gid); if (ret != EOK) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "chown failed for [%s] [%d]: %s\n", filename, ret, sss_strerror(ret)); return ret; } } return EOK; } static DBusServer * sbus_server_setup_dbus(TALLOC_CTX *mem_ctx, struct tevent_context *ev, const char *address, bool use_symlink, uid_t uid, gid_t gid, const char **_symlink) { TALLOC_CTX *tmp_ctx; DBusServer *dbus_server = NULL; bool symlink_created = false; const char *symlink = NULL; const char *socket_address; const char *filename; errno_t ret; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!\n"); return NULL; } /* Get socket address. */ ret = sbus_server_get_socket(tmp_ctx, address, use_symlink, &socket_address, &filename, &symlink); if (ret != EOK) { goto done; } /* Start listening on this socket. This will also create the socket. */ dbus_server = sbus_server_socket_listen(socket_address); if (dbus_server == NULL) { ret = EIO; goto done; } /* Create symlink if requested. */ if (use_symlink) { ret = sbus_server_symlink_create(filename, symlink); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Could not create symlink [%d]: %s\n", ret, sss_strerror(ret)); ret = EIO; goto done; } symlink_created = true; } /* Check file permissions and setup proper owner. */ ret = sbus_server_check_file(filename, uid, gid); if (ret != EOK) { goto done; } if (use_symlink) { *_symlink = talloc_strdup(mem_ctx, symlink); if (*_symlink == NULL) { ret = ENOMEM; goto done; } } else { *_symlink = NULL; } ret = EOK; done: talloc_free(tmp_ctx); if (ret != EOK && dbus_server != NULL) { dbus_server_disconnect(dbus_server); dbus_server_unref(dbus_server); if (symlink_created) { sbus_server_symlink_remove(symlink); } return NULL; } return dbus_server; } static bool sbus_server_filter_add(struct sbus_server *server, DBusConnection *dbus_conn) { dbus_bool_t dbret; /* Add a connection filter that is used to process input messages. */ dbret = dbus_connection_add_filter(dbus_conn, sbus_server_filter, server, NULL); if (dbret == false) { return false; } return true; } static dbus_bool_t sbus_server_check_connection_uid(DBusConnection *dbus_conn, unsigned long uid, void *data) { struct sbus_server *sbus_server; sbus_server = talloc_get_type(data, struct sbus_server); if (uid == 0 || uid == sbus_server->uid) { return true; } return false; } static void sbus_server_new_connection(DBusServer *dbus_server, DBusConnection *dbus_conn, void *data) { struct sbus_server *sbus_server; struct sbus_connection *sbus_conn; dbus_bool_t dbret; errno_t ret; bool bret; sbus_server = talloc_get_type(data, struct sbus_server); DEBUG(SSSDBG_FUNC_DATA, "Adding connection %p.\n", dbus_conn); /* Allow access from uid that is associated with this sbus server. */ dbus_connection_set_unix_user_function(dbus_conn, sbus_server_check_connection_uid, sbus_server, NULL); /* First, add a message filter that will take care of routing messages * between connections. */ bret = sbus_server_filter_add(sbus_server, dbus_conn); if (!bret) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to add server filter!\n"); return; } /** * @dbus_conn is unreferenced in libdbus by the caller of this new * connection function thus we must not unreference it here. Its * reference counter is increased in @sbus_connection_init. */ sbus_conn = sbus_connection_init(sbus_server, sbus_server->ev, dbus_conn, NULL, NULL, SBUS_CONNECTION_CLIENT, NULL); if (sbus_conn == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Closing connection, unable to setup\n"); dbus_connection_close(dbus_conn); return; } dbret = dbus_connection_set_data(dbus_conn, sbus_server->data_slot, sbus_conn, NULL); if (!dbret) { DEBUG(SSSDBG_CRIT_FAILURE, "Closing connection, unable to set data\n"); talloc_free(sbus_conn); return; } if (sbus_server->on_connection->callback != NULL) { ret = sbus_server->on_connection->callback(sbus_conn, sbus_server->on_connection->data); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Closing connection, new connection " "callback failed [%d]: %s\n", ret, sss_strerror(ret)); talloc_free(sbus_conn); return; } } } static errno_t sbus_server_tevent_enable(struct sbus_server *server) { errno_t ret; ret = sbus_watch_server(server, server->ev, server->server, &server->watch_ctx); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup D-Bus watch [%d]: %s\n", ret, sss_strerror(ret)); return ret; } /* Set function that is called each time a new client is connected. */ dbus_server_set_new_connection_function(server->server, sbus_server_new_connection, server, NULL); return EOK; } static void sbus_server_tevent_disable(struct sbus_server *server) { dbus_server_set_new_connection_function(server->server, NULL, NULL, NULL); talloc_zfree(server->watch_ctx); } static void sbus_server_name_owner_changed(struct sbus_server *server, const char *name, const char *new_owner, const char *old_owner) { DBusMessage *message; /* We can't really send signals when the server is being destroyed. */ if (server == NULL || server->disconnecting) { return; } message = sbus_signal_create(NULL, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, "NameOwnerChanged", DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &new_owner, DBUS_TYPE_STRING, &old_owner); if (message == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); return; } dbus_message_set_sender(message, DBUS_SERVICE_DBUS); /* Send the signal. */ sbus_server_matchmaker(server, NULL, name, message); } void sbus_server_name_acquired(struct sbus_server *server, struct sbus_connection *conn, const char *name) { DBusMessage *message; message = sbus_signal_create(NULL, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, "NameAcquired", DBUS_TYPE_STRING, &name); if (message == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); return; } dbus_message_set_sender(message, DBUS_SERVICE_DBUS); dbus_message_set_destination(message, conn->unique_name); dbus_connection_send(conn->connection, message, NULL); sbus_server_name_owner_changed(server, name, name, ""); } void sbus_server_name_lost(struct sbus_server *server, struct sbus_connection *conn, const char *name) { DBusMessage *message; if (name[0] == ':') { /* The connection is being terminated. Do not send the signal. */ return; } message = sbus_signal_create(NULL, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, "NameLost", DBUS_TYPE_STRING, &name); if (message == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); return; } dbus_message_set_sender(message, DBUS_SERVICE_DBUS); dbus_message_set_destination(message, conn->unique_name); dbus_connection_send(conn->connection, message, NULL); sbus_server_name_owner_changed(server, name, "", name); } static void sbus_server_name_remove_from_table_cb(hash_entry_t *item, hash_destroy_enum type, void *pvt) { struct sbus_server *server; const char *name; /* We can't really send signals when the server is being destroyed. */ if (type == HASH_TABLE_DESTROY) { return; } server = talloc_get_type(pvt, struct sbus_server); name = item->key.str; sbus_server_name_owner_changed(server, name, "", name); } static int sbus_server_destructor(struct sbus_server *server) { if (server->server == NULL) { return 0; } server->disconnecting = true; /* Remove tevent integration first. */ sbus_server_tevent_disable(server); if (server->data_slot != -1) { dbus_connection_free_data_slot(&server->data_slot); } /* Release server. */ dbus_server_disconnect(server->server); dbus_server_unref(server->server); if (server->symlink != NULL) { sbus_server_symlink_remove(server->symlink); } return 0; } struct sbus_server * sbus_server_create(TALLOC_CTX *mem_ctx, struct tevent_context *ev, const char *address, bool use_symlink, uint32_t max_connections, uid_t uid, gid_t gid, sbus_server_on_connection_cb on_conn_cb, sbus_server_on_connection_data on_conn_data) { DBusServer *dbus_server; struct sbus_server *sbus_server; const char *symlink; dbus_bool_t dbret; errno_t ret; sbus_server = talloc_zero(mem_ctx, struct sbus_server); if (sbus_server == NULL) { return NULL; } sbus_server->data_slot = -1; talloc_set_destructor(sbus_server, sbus_server_destructor); dbus_server = sbus_server_setup_dbus(sbus_server, ev, address, use_symlink, uid, gid, &symlink); if (dbus_server == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup a D-Bus server!\n"); ret = ENOMEM; goto done; } sbus_server->ev = ev; sbus_server->server = dbus_server; sbus_server->symlink = talloc_steal(sbus_server, symlink); sbus_server->max_connections = max_connections; sbus_server->name.major = 1; sbus_server->name.minor = 0; sbus_server->uid = uid; sbus_server->gid = gid; sbus_server->on_connection = talloc_zero(sbus_server, struct sbus_server_on_connection); if (sbus_server->on_connection == NULL) { ret = ENOMEM; goto done; } if (on_conn_cb != NULL) { _sbus_server_set_on_connection(sbus_server, "on-connection", on_conn_cb, on_conn_data); } sbus_server->names = sss_ptr_hash_create(sbus_server, sbus_server_name_remove_from_table_cb, sbus_server); if (sbus_server->names == NULL) { ret = ENOMEM; goto done; } sbus_server->match_rules = sss_ptr_hash_create(sbus_server, NULL, NULL); if (sbus_server->match_rules == NULL) { ret = ENOMEM; goto done; } sbus_server->router = sbus_router_init(sbus_server, NULL); if (sbus_server->router == NULL) { ret = ENOMEM; goto done; } ret = sbus_server_setup_interface(sbus_server); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup bus interface [%d]: %s\n", ret, sss_strerror(ret)); goto done; } dbret = dbus_connection_allocate_data_slot(&sbus_server->data_slot); if (!dbret) { ret = ENOMEM; goto done; } ret = sbus_server_tevent_enable(sbus_server); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Unable to integrate with tevent [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = EOK; done: if (ret != EOK) { talloc_free(sbus_server); return NULL; } return sbus_server; } struct sbus_connection * sbus_server_find_connection(struct sbus_server *server, const char *name) { return sss_ptr_hash_lookup(server->names, name, struct sbus_connection); } void _sbus_server_set_on_connection(struct sbus_server *server, const char *name, sbus_server_on_connection_cb on_connection_cb, sbus_server_on_connection_data data) { if (server == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Bug: server is NULL\n"); return; } if (name == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Bug: name is NULL\n"); return; } if (on_connection_cb == NULL) { DEBUG(SSSDBG_TRACE_FUNC, "Unsetting on connectoin callback\n"); server->on_connection->callback = NULL; server->on_connection->data = NULL; server->on_connection->name = NULL; return; } if (server->on_connection->callback != NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Bug: on connection callback is " "already set to %s\n", server->on_connection->name); return; } server->on_connection->callback = on_connection_cb; server->on_connection->data = data; server->on_connection->name = name; }