2005-01-18 Havoc Pennington <hp@redhat.com>
[platform/upstream/dbus.git] / bus / services.c
index 9508b2f..90c8d1d 100644 (file)
@@ -4,7 +4,7 @@
  * Copyright (C) 2003  Red Hat, Inc.
  * Copyright (C) 2003  CodeFactory AB
  *
- * Licensed under the Academic Free License version 1.2
+ * Licensed under the Academic Free License version 2.1
  * 
  * 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
 #include "services.h"
 #include "connection.h"
 #include "utils.h"
+#include "activation.h"
+#include "policy.h"
+#include "bus.h"
+#include "selinux.h"
 
 struct BusService
 {
+  int refcount;
+
   BusRegistry *registry;
   char *name;
   DBusList *owners;
@@ -42,13 +48,17 @@ struct BusService
 struct BusRegistry
 {
   int refcount;
+
+  BusContext *context;
   
   DBusHashTable *service_hash;
   DBusMemPool   *service_pool;
+
+  DBusHashTable *service_sid_table;
 };
 
 BusRegistry*
-bus_registry_new (void)
+bus_registry_new (BusContext *context)
 {
   BusRegistry *registry;
 
@@ -57,7 +67,8 @@ bus_registry_new (void)
     return NULL;
 
   registry->refcount = 1;
-
+  registry->context = context;
+  
   registry->service_hash = _dbus_hash_table_new (DBUS_HASH_STRING,
                                                  NULL, NULL);
   if (registry->service_hash == NULL)
@@ -68,6 +79,8 @@ bus_registry_new (void)
   if (registry->service_pool == NULL)
     goto failed;
 
+  registry->service_sid_table = NULL;
+  
   return registry;
 
  failed:
@@ -75,11 +88,13 @@ bus_registry_new (void)
   return NULL;
 }
 
-void
+BusRegistry *
 bus_registry_ref (BusRegistry *registry)
 {
   _dbus_assert (registry->refcount > 0);
   registry->refcount += 1;
+
+  return registry;
 }
 
 void
@@ -94,7 +109,9 @@ bus_registry_unref  (BusRegistry *registry)
         _dbus_hash_table_unref (registry->service_hash);
       if (registry->service_pool)
         _dbus_mem_pool_free (registry->service_pool);
-
+      if (registry->service_sid_table)
+        _dbus_hash_table_unref (registry->service_sid_table);
+      
       dbus_free (registry);
     }
 }
@@ -103,13 +120,10 @@ BusService*
 bus_registry_lookup (BusRegistry      *registry,
                      const DBusString *service_name)
 {
-  const char *c_name;
   BusService *service;
-  
-  _dbus_string_get_const_data (service_name, &c_name);
 
   service = _dbus_hash_table_lookup_string (registry->service_hash,
-                                            c_name);
+                                            _dbus_string_get_const_data (service_name));
 
   return service;
 }
@@ -121,16 +135,15 @@ bus_registry_ensure (BusRegistry               *registry,
                      BusTransaction            *transaction,
                      DBusError                 *error)
 {
-  const char *c_name;
   BusService *service;
 
+  _DBUS_ASSERT_ERROR_IS_CLEAR (error);
+  
   _dbus_assert (owner_if_created != NULL);
   _dbus_assert (transaction != NULL);
-  
-  _dbus_string_get_const_data (service_name, &c_name);
 
   service = _dbus_hash_table_lookup_string (registry->service_hash,
-                                            c_name);
+                                            _dbus_string_get_const_data (service_name));
   if (service != NULL)
     return service;
   
@@ -142,27 +155,40 @@ bus_registry_ensure (BusRegistry               *registry,
     }
 
   service->registry = registry;  
+  service->refcount = 1;
 
-  service->name = _dbus_strdup (c_name);
-  if (service->name == NULL)
+  _dbus_verbose ("copying string %p '%s' to service->name\n",
+                 service_name, _dbus_string_get_const_data (service_name));
+  if (!_dbus_string_copy_data (service_name, &service->name))
     {
       _dbus_mem_pool_dealloc (registry->service_pool, service);
       BUS_SET_OOM (error);
       return NULL;
     }
-
-  if (!bus_driver_send_service_created (service->name, transaction, error))
+  _dbus_verbose ("copied string %p '%s' to '%s'\n",
+                 service_name, _dbus_string_get_const_data (service_name),
+                 service->name);
+
+  if (!bus_driver_send_service_owner_changed (service->name, 
+                                             NULL,
+                                             bus_connection_get_name (owner_if_created),
+                                             transaction, error))
     {
-      dbus_free (service->name);
-      _dbus_mem_pool_dealloc (registry->service_pool, service);
+      bus_service_unref (service);
       return NULL;
     }
 
+  if (!bus_activation_service_created (bus_context_get_activation (registry->context),
+                                      service->name, transaction, error))
+    {
+      bus_service_unref (service);
+      return NULL;
+    }
+  
   if (!bus_service_add_owner (service, owner_if_created,
                               transaction, error))
     {
-      dbus_free (service->name);
-      _dbus_mem_pool_dealloc (registry->service_pool, service);
+      bus_service_unref (service);
       return NULL;
     }
   
@@ -170,9 +196,7 @@ bus_registry_ensure (BusRegistry               *registry,
                                        service->name,
                                        service))
     {
-      _dbus_list_clear (&service->owners);
-      dbus_free (service->name);
-      _dbus_mem_pool_dealloc (registry->service_pool, service);
+      /* The add_owner gets reverted on transaction cancel */
       BUS_SET_OOM (error);
       return NULL;
     }
@@ -241,11 +265,303 @@ bus_registry_list_services (BusRegistry *registry,
 }
 
 dbus_bool_t
+bus_registry_acquire_service (BusRegistry      *registry,
+                              DBusConnection   *connection,
+                              const DBusString *service_name,
+                              dbus_uint32_t     flags,
+                              dbus_uint32_t    *result,
+                              BusTransaction   *transaction,
+                              DBusError        *error)
+{
+  dbus_bool_t retval;
+  DBusConnection *old_owner;
+  DBusConnection *current_owner;
+  BusClientPolicy *policy;
+  BusService *service;
+  BusActivation  *activation;
+  BusSELinuxID *sid;
+  
+  retval = FALSE;
+
+  if (_dbus_string_get_length (service_name) == 0)
+    {
+      dbus_set_error (error, DBUS_ERROR_ACCESS_DENIED,
+                      "Zero-length service name is not allowed");
+      
+      _dbus_verbose ("Attempt to acquire zero-length service name\n");
+      
+      goto out;
+    }
+  
+  if (_dbus_string_get_byte (service_name, 0) == ':')
+    {
+      /* Not allowed; only base services can start with ':' */
+      dbus_set_error (error, DBUS_ERROR_ACCESS_DENIED,
+                      "Cannot acquire a service starting with ':' such as \"%s\"",
+                      _dbus_string_get_const_data (service_name));
+      
+      _dbus_verbose ("Attempt to acquire invalid base service name \"%s\"",
+                     _dbus_string_get_const_data (service_name));
+      
+      goto out;
+    }
+
+  policy = bus_connection_get_policy (connection);
+  _dbus_assert (policy != NULL);
+
+  /* Note that if sid is #NULL then the bus's own context gets used
+   * in bus_connection_selinux_allows_acquire_service()
+   */
+  sid = bus_selinux_id_table_lookup (registry->service_sid_table,
+                                     service_name);
+
+  if (!bus_selinux_allows_acquire_service (connection, sid,
+                                          _dbus_string_get_const_data (service_name)))
+    {
+      dbus_set_error (error, DBUS_ERROR_ACCESS_DENIED,
+                      "Connection \"%s\" is not allowed to own the service \"%s\" due "
+                      "to SELinux policy",
+                      bus_connection_is_active (connection) ?
+                      bus_connection_get_name (connection) :
+                      "(inactive)",
+                      _dbus_string_get_const_data (service_name));
+      goto out;
+    }
+      
+  if (!bus_client_policy_check_can_own (policy, connection,
+                                        service_name))
+    {
+      dbus_set_error (error, DBUS_ERROR_ACCESS_DENIED,
+                      "Connection \"%s\" is not allowed to own the service \"%s\" due "
+                      "to security policies in the configuration file",
+                      bus_connection_is_active (connection) ?
+                      bus_connection_get_name (connection) :
+                      "(inactive)",
+                      _dbus_string_get_const_data (service_name));
+      goto out;
+    }
+
+  if (bus_connection_get_n_services_owned (connection) >=
+      bus_context_get_max_services_per_connection (registry->context))
+    {
+      dbus_set_error (error, DBUS_ERROR_LIMITS_EXCEEDED,
+                      "Connection \"%s\" is not allowed to own more services "
+                      "(increase limits in configuration file if required)",
+                      bus_connection_is_active (connection) ?
+                      bus_connection_get_name (connection) :
+                      "(inactive)");
+      goto out;
+    }
+  
+  service = bus_registry_lookup (registry, service_name);
+
+  if (service != NULL)
+    old_owner = bus_service_get_primary_owner (service);
+  else
+    old_owner = NULL;
+      
+  if (service == NULL)
+    {
+      service = bus_registry_ensure (registry,
+                                     service_name, connection, transaction, error);
+      if (service == NULL)
+        goto out;
+    }
+
+  current_owner = bus_service_get_primary_owner (service);
+
+  if (old_owner == NULL)
+    {
+      _dbus_assert (current_owner == connection);
+
+      bus_service_set_prohibit_replacement (service,
+                                           (flags & DBUS_NAME_FLAG_PROHIBIT_REPLACEMENT));      
+                       
+      *result = DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER;      
+    }
+  else if (old_owner == connection)
+    *result = DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER;
+  else if (!((flags & DBUS_NAME_FLAG_REPLACE_EXISTING)))
+    *result = DBUS_REQUEST_NAME_REPLY_EXISTS;
+  else if (bus_service_get_prohibit_replacement (service))
+    {
+      /* Queue the connection */
+      if (!bus_service_add_owner (service, connection,
+                                  transaction, error))
+        goto out;
+      
+      *result = DBUS_REQUEST_NAME_REPLY_IN_QUEUE;
+    }
+  else
+    {
+      /* Replace the current owner */
+
+      /* We enqueue the new owner and remove the first one because
+       * that will cause NameAcquired and NameLost messages to
+       * be sent.
+       */
+      
+      if (!bus_service_add_owner (service, connection,
+                                  transaction, error))
+        goto out;
+
+      if (!bus_service_remove_owner (service, old_owner,
+                                     transaction, error))
+        goto out;
+      
+      _dbus_assert (connection == bus_service_get_primary_owner (service));
+      *result = DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER;
+    }
+
+  activation = bus_context_get_activation (registry->context);
+  retval = bus_activation_send_pending_auto_activation_messages (activation,
+                                                                service,
+                                                                transaction,
+                                                                error);
+  
+ out:
+  return retval;
+}
+
+dbus_bool_t
+bus_registry_set_service_context_table (BusRegistry   *registry,
+                                       DBusHashTable *table)
+{
+  DBusHashTable *new_table;
+  DBusHashIter iter;
+  
+  new_table = bus_selinux_id_table_new ();
+  if (!new_table)
+    return FALSE;
+
+  _dbus_hash_iter_init (table, &iter);
+  while (_dbus_hash_iter_next (&iter))
+    {
+      const char *service = _dbus_hash_iter_get_string_key (&iter);
+      const char *context = _dbus_hash_iter_get_value (&iter);
+
+      if (!bus_selinux_id_table_insert (new_table,
+                                       service,
+                                       context))
+       return FALSE;
+    }
+  
+  if (registry->service_sid_table)
+    _dbus_hash_table_unref (registry->service_sid_table);
+  registry->service_sid_table = new_table;
+  return TRUE;
+}
+
+static void
+bus_service_unlink_owner (BusService      *service,
+                          DBusConnection  *owner)
+{
+  _dbus_list_remove_last (&service->owners, owner);
+  bus_connection_remove_owned_service (owner, service);
+}
+
+static void
+bus_service_unlink (BusService *service)
+{
+  _dbus_assert (service->owners == NULL);
+
+  /* the service may not be in the hash, if
+   * the failure causing transaction cancel
+   * was in the right place, but that's OK
+   */
+  _dbus_hash_table_remove_string (service->registry->service_hash,
+                                  service->name);
+  
+  bus_service_unref (service);
+}
+
+static void
+bus_service_relink (BusService           *service,
+                    DBusPreallocatedHash *preallocated)
+{
+  _dbus_assert (service->owners == NULL);
+  _dbus_assert (preallocated != NULL);
+
+  _dbus_hash_table_insert_string_preallocated (service->registry->service_hash,
+                                               preallocated,
+                                               service->name,
+                                               service);
+  
+  bus_service_ref (service);
+}
+
+/**
+ * Data used to represent an ownership cancellation in
+ * a bus transaction.
+ */
+typedef struct
+{
+  DBusConnection *connection; /**< the connection */
+  BusService *service;        /**< service to cancel ownership of */
+} OwnershipCancelData;
+
+static void
+cancel_ownership (void *data)
+{
+  OwnershipCancelData *d = data;
+
+  /* We don't need to send messages notifying of these
+   * changes, since we're reverting something that was
+   * cancelled (effectively never really happened)
+   */
+  bus_service_unlink_owner (d->service, d->connection);
+  
+  if (d->service->owners == NULL)
+    bus_service_unlink (d->service);
+}
+
+static void
+free_ownership_cancel_data (void *data)
+{
+  OwnershipCancelData *d = data;
+
+  dbus_connection_unref (d->connection);
+  bus_service_unref (d->service);
+  
+  dbus_free (d);
+}
+
+static dbus_bool_t
+add_cancel_ownership_to_transaction (BusTransaction *transaction,
+                                     BusService     *service,
+                                     DBusConnection *connection)
+{
+  OwnershipCancelData *d;
+
+  d = dbus_new (OwnershipCancelData, 1);
+  if (d == NULL)
+    return FALSE;
+  
+  d->service = service;
+  d->connection = connection;
+
+  if (!bus_transaction_add_cancel_hook (transaction, cancel_ownership, d,
+                                        free_ownership_cancel_data))
+    {
+      dbus_free (d);
+      return FALSE;
+    }
+
+  bus_service_ref (d->service);
+  dbus_connection_ref (d->connection);
+  
+  return TRUE;
+}
+
+/* this function is self-cancelling if you cancel the transaction */
+dbus_bool_t
 bus_service_add_owner (BusService     *service,
                        DBusConnection *owner,
                        BusTransaction *transaction,
                        DBusError      *error)
 {
+  _DBUS_ASSERT_ERROR_IS_CLEAR (error);
+  
  /* Send service acquired message first, OOM will result
   * in cancelling the transaction
   */
@@ -268,16 +584,155 @@ bus_service_add_owner (BusService     *service,
       BUS_SET_OOM (error);
       return FALSE;
     }
+
+  if (!add_cancel_ownership_to_transaction (transaction,
+                                            service,
+                                            owner))
+    {
+      bus_service_unlink_owner (service, owner);
+      BUS_SET_OOM (error);
+      return FALSE;
+    }
   
   return TRUE;
 }
 
+typedef struct
+{
+  DBusConnection *connection;
+  BusService     *service;
+  DBusConnection *before_connection; /* restore to position before this connection in owners list */
+  DBusList       *connection_link;
+  DBusList       *service_link;
+  DBusPreallocatedHash *hash_entry;
+} OwnershipRestoreData;
+
+static void
+restore_ownership (void *data)
+{
+  OwnershipRestoreData *d = data;
+  DBusList *link;
+
+  _dbus_assert (d->service_link != NULL);
+  _dbus_assert (d->connection_link != NULL);
+  
+  if (d->service->owners == NULL)
+    {
+      _dbus_assert (d->hash_entry != NULL);
+      bus_service_relink (d->service, d->hash_entry);
+    }
+  else
+    {
+      _dbus_assert (d->hash_entry == NULL);
+    }
+  
+  /* We don't need to send messages notifying of these
+   * changes, since we're reverting something that was
+   * cancelled (effectively never really happened)
+   */
+  link = _dbus_list_get_first_link (&d->service->owners);
+  while (link != NULL)
+    {
+      if (link->data == d->before_connection)
+        break;
+
+      link = _dbus_list_get_next_link (&d->service->owners, link);
+    }
+  
+  _dbus_list_insert_before_link (&d->service->owners, link, d->connection_link);
+
+  /* Note that removing then restoring this changes the order in which
+   * ServiceDeleted messages are sent on destruction of the
+   * connection.  This should be OK as the only guarantee there is
+   * that the base service is destroyed last, and we never even
+   * tentatively remove the base service.
+   */
+  bus_connection_add_owned_service_link (d->connection, d->service_link);
+  
+  d->hash_entry = NULL;
+  d->service_link = NULL;
+  d->connection_link = NULL;
+}
+
+static void
+free_ownership_restore_data (void *data)
+{
+  OwnershipRestoreData *d = data;
+
+  if (d->service_link)
+    _dbus_list_free_link (d->service_link);
+  if (d->connection_link)
+    _dbus_list_free_link (d->connection_link);
+  if (d->hash_entry)
+    _dbus_hash_table_free_preallocated_entry (d->service->registry->service_hash,
+                                              d->hash_entry);
+
+  dbus_connection_unref (d->connection);
+  bus_service_unref (d->service);
+  
+  dbus_free (d);
+}
+
+static dbus_bool_t
+add_restore_ownership_to_transaction (BusTransaction *transaction,
+                                      BusService     *service,
+                                      DBusConnection *connection)
+{
+  OwnershipRestoreData *d;
+  DBusList *link;
+
+  d = dbus_new (OwnershipRestoreData, 1);
+  if (d == NULL)
+    return FALSE;
+  
+  d->service = service;
+  d->connection = connection;
+  d->service_link = _dbus_list_alloc_link (service);
+  d->connection_link = _dbus_list_alloc_link (connection);
+  d->hash_entry = _dbus_hash_table_preallocate_entry (service->registry->service_hash);
+  
+  bus_service_ref (d->service);
+  dbus_connection_ref (d->connection);
+
+  d->before_connection = NULL;
+  link = _dbus_list_get_first_link (&service->owners);
+  while (link != NULL)
+    {
+      if (link->data == connection)
+        {
+          link = _dbus_list_get_next_link (&service->owners, link);
+
+          if (link)
+            d->before_connection = link->data;
+
+          break;
+        }
+      
+      link = _dbus_list_get_next_link (&service->owners, link);
+    }
+  
+  if (d->service_link == NULL ||
+      d->connection_link == NULL ||
+      d->hash_entry == NULL ||
+      !bus_transaction_add_cancel_hook (transaction, restore_ownership, d,
+                                        free_ownership_restore_data))
+    {
+      free_ownership_restore_data (d);
+      return FALSE;
+    }
+  
+  return TRUE;
+}
+
+/* this function is self-cancelling if you cancel the transaction */
 dbus_bool_t
 bus_service_remove_owner (BusService     *service,
                           DBusConnection *owner,
                           BusTransaction *transaction,
                           DBusError      *error)
 {
+  _DBUS_ASSERT_ERROR_IS_CLEAR (error);
+  
   /* We send out notifications before we do any work we
    * might have to undo if the notification-sending failed
    */
@@ -290,43 +745,81 @@ bus_service_remove_owner (BusService     *service,
         return FALSE;
     }
 
-  if (_dbus_list_length_is_one (&service->owners))
+  if (service->owners == NULL)
     {
-      /* We are the only owner - send service deleted */
-      if (!bus_driver_send_service_deleted (service->name,
-                                            transaction, error))
+      _dbus_assert_not_reached ("Tried to remove owner of a service that has no owners");
+    }
+  else if (_dbus_list_length_is_one (&service->owners))
+    {
+      if (!bus_driver_send_service_owner_changed (service->name,
+                                                 bus_connection_get_name (owner),
+                                                 NULL,
+                                                 transaction, error))
         return FALSE;
     }
   else
     {
       DBusList *link;
-      link = _dbus_list_get_first (&service->owners);
+      DBusConnection *new_owner;
+      link = _dbus_list_get_first_link (&service->owners);
+      _dbus_assert (link != NULL);
       link = _dbus_list_get_next_link (&service->owners, link);
+      _dbus_assert (link != NULL);
 
-      if (link != NULL)
-        {
-          /* This will be our new owner */
-          if (!bus_driver_send_service_acquired (link->data,
-                                                 service->name,
-                                                 transaction,
-                                                 error))
-            return FALSE;
-        }
+      new_owner = link->data;
+
+      if (!bus_driver_send_service_owner_changed (service->name,
+                                                 bus_connection_get_name (owner),
+                                                 bus_connection_get_name (new_owner),
+                                                 transaction, error))
+        return FALSE;
+
+      /* This will be our new owner */
+      if (!bus_driver_send_service_acquired (new_owner,
+                                             service->name,
+                                             transaction,
+                                             error))
+        return FALSE;
+    }
+
+  if (!add_restore_ownership_to_transaction (transaction, service, owner))
+    {
+      BUS_SET_OOM (error);
+      return FALSE;
     }
   
-  _dbus_list_remove_last (&service->owners, owner);
-  bus_connection_remove_owned_service (owner, service);
+  bus_service_unlink_owner (service, owner);
 
   if (service->owners == NULL)
+    bus_service_unlink (service);
+
+  return TRUE;
+}
+
+BusService *
+bus_service_ref (BusService *service)
+{
+  _dbus_assert (service->refcount > 0);
+  
+  service->refcount += 1;
+
+  return service;
+}
+
+void
+bus_service_unref (BusService *service)
+{
+  _dbus_assert (service->refcount > 0);
+  
+  service->refcount -= 1;
+
+  if (service->refcount == 0)
     {
-      /* Delete service (already sent message that it was deleted above) */
-      _dbus_hash_table_remove_string (service->registry->service_hash, service->name);
+      _dbus_assert (service->owners == NULL);
       
       dbus_free (service->name);
       _dbus_mem_pool_dealloc (service->registry->service_pool, service);
     }
-
-  return TRUE;
 }
 
 DBusConnection*