| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2015 Google Inc. |
| * |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <limits.h> |
| |
| #include "bluetooth/bluetooth.h" |
| #include "bluetooth/sdp.h" |
| #include "bluetooth/sdp_lib.h" |
| #include "bluetooth/uuid.h" |
| #include "bluetooth/mgmt.h" |
| #include "btio/btio.h" |
| #include "gdbus/gdbus.h" |
| #include "src/shared/util.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/io.h" |
| #include "src/shared/att.h" |
| #include "src/shared/gatt-db.h" |
| #include "src/shared/gatt-server.h" |
| #include "log.h" |
| #include "error.h" |
| #include "btd.h" |
| #include "adapter.h" |
| #include "device.h" |
| #include "gatt-database.h" |
| #include "dbus-common.h" |
| #include "profile.h" |
| #include "service.h" |
| #include "textfile.h" |
| #include "settings.h" |
| |
| #define GATT_MANAGER_IFACE "org.bluez.GattManager1" |
| #define GATT_PROFILE_IFACE "org.bluez.GattProfile1" |
| #define GATT_SERVICE_IFACE "org.bluez.GattService1" |
| #define GATT_CHRC_IFACE "org.bluez.GattCharacteristic1" |
| #define GATT_DESC_IFACE "org.bluez.GattDescriptor1" |
| #define ERROR_FAILED ERROR_INTERFACE ".Failed" |
| |
| #define UUID_GAP 0x1800 |
| #define UUID_GATT 0x1801 |
| #define UUID_DIS 0x180a |
| |
| #ifndef MIN |
| #define MIN(a, b) ((a) < (b) ? (a) : (b)) |
| #endif |
| |
| struct gatt_record { |
| struct btd_gatt_database *database; |
| uint32_t handle; |
| struct gatt_db_attribute *attr; |
| }; |
| |
| struct btd_gatt_database { |
| struct btd_adapter *adapter; |
| struct gatt_db *db; |
| unsigned int db_id; |
| GIOChannel *le_io; |
| GIOChannel *eatt_io; |
| GIOChannel *bredr_io; |
| struct queue *records; |
| struct queue *device_states; |
| struct queue *ccc_callbacks; |
| struct gatt_db_attribute *svc_chngd; |
| struct gatt_db_attribute *svc_chngd_ccc; |
| struct gatt_db_attribute *cli_feat; |
| struct gatt_db_attribute *db_hash; |
| struct gatt_db_attribute *eatt; |
| struct queue *apps; |
| struct queue *profiles; |
| }; |
| |
| struct gatt_app { |
| struct btd_gatt_database *database; |
| char *owner; |
| char *path; |
| DBusMessage *reg; |
| GDBusClient *client; |
| bool failed; |
| struct queue *profiles; |
| struct queue *services; |
| struct queue *proxies; |
| }; |
| |
| struct external_service { |
| struct gatt_app *app; |
| char *path; /* Path to GattService1 */ |
| GDBusProxy *proxy; |
| struct gatt_db_attribute *attrib; |
| uint16_t attr_cnt; |
| struct queue *chrcs; |
| struct queue *descs; |
| struct queue *includes; |
| }; |
| |
| struct external_profile { |
| struct gatt_app *app; |
| GDBusProxy *proxy; |
| struct queue *profiles; /* btd_profile list */ |
| }; |
| |
| struct client_io { |
| struct bt_att *att; |
| struct external_chrc *chrc; |
| unsigned int disconn_id; |
| struct io *io; |
| }; |
| |
| struct external_chrc { |
| struct external_service *service; |
| char *path; |
| GDBusProxy *proxy; |
| uint8_t props; |
| uint8_t ext_props; |
| uint32_t perm; |
| uint32_t ccc_perm; |
| uint16_t mtu; |
| struct queue *write_ios; |
| struct queue *notify_ios; |
| struct gatt_db_attribute *attrib; |
| struct gatt_db_attribute *ccc; |
| struct queue *pending_reads; |
| struct queue *pending_writes; |
| unsigned int ntfy_cnt; |
| bool prep_authorized; |
| bool req_prep_authorization; |
| }; |
| |
| struct external_desc { |
| struct external_service *service; |
| char *chrc_path; |
| GDBusProxy *proxy; |
| uint32_t perm; |
| struct gatt_db_attribute *attrib; |
| bool handled; |
| struct queue *pending_reads; |
| struct queue *pending_writes; |
| bool prep_authorized; |
| bool req_prep_authorization; |
| }; |
| |
| struct pending_op { |
| struct bt_att *att; |
| unsigned int id; |
| unsigned int disconn_id; |
| uint16_t offset; |
| uint8_t link_type; |
| struct gatt_db_attribute *attrib; |
| struct queue *owner_queue; |
| struct iovec data; |
| bool is_characteristic; |
| bool prep_authorize; |
| }; |
| |
| struct notify { |
| struct btd_gatt_database *database; |
| uint16_t handle, ccc_handle; |
| uint8_t *value; |
| uint16_t len; |
| bt_gatt_server_conf_func_t conf; |
| void *user_data; |
| }; |
| |
| #define CLI_FEAT_SIZE 1 |
| |
| struct device_state { |
| struct btd_gatt_database *db; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| unsigned int disc_id; |
| uint8_t cli_feat[CLI_FEAT_SIZE]; |
| bool change_aware; |
| bool out_of_sync; |
| struct queue *ccc_states; |
| struct notify *pending; |
| }; |
| |
| typedef uint8_t (*btd_gatt_database_ccc_write_t) (struct pending_op *op, |
| void *user_data); |
| typedef void (*btd_gatt_database_destroy_t) (void *data); |
| |
| struct ccc_state { |
| uint16_t handle; |
| uint16_t value; |
| }; |
| |
| struct ccc_cb_data { |
| uint16_t handle; |
| btd_gatt_database_ccc_write_t callback; |
| btd_gatt_database_destroy_t destroy; |
| void *user_data; |
| }; |
| |
| struct device_info { |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| }; |
| |
| static struct queue *dbs = NULL; |
| |
| static void ccc_cb_free(void *data) |
| { |
| struct ccc_cb_data *ccc_cb = data; |
| |
| if (ccc_cb->destroy) |
| ccc_cb->destroy(ccc_cb->user_data); |
| |
| free(ccc_cb); |
| } |
| |
| static bool ccc_cb_match_service(const void *data, const void *match_data) |
| { |
| const struct ccc_cb_data *ccc_cb = data; |
| const struct gatt_db_attribute *attrib = match_data; |
| uint16_t start, end; |
| |
| if (!gatt_db_attribute_get_service_handles(attrib, &start, &end)) |
| return false; |
| |
| return ccc_cb->handle >= start && ccc_cb->handle <= end; |
| } |
| |
| static bool ccc_cb_match_handle(const void *data, const void *match_data) |
| { |
| const struct ccc_cb_data *ccc_cb = data; |
| uint16_t handle = PTR_TO_UINT(match_data); |
| |
| return ccc_cb->handle == handle; |
| } |
| |
| static bool dev_state_match(const void *a, const void *b) |
| { |
| const struct device_state *dev_state = a; |
| const struct device_info *dev_info = b; |
| |
| return bacmp(&dev_state->bdaddr, &dev_info->bdaddr) == 0 && |
| dev_state->bdaddr_type == dev_info->bdaddr_type; |
| } |
| |
| static struct device_state * |
| find_device_state(struct btd_gatt_database *database, const bdaddr_t *bdaddr, |
| uint8_t bdaddr_type) |
| { |
| struct device_info dev_info; |
| |
| memset(&dev_info, 0, sizeof(dev_info)); |
| |
| bacpy(&dev_info.bdaddr, bdaddr); |
| dev_info.bdaddr_type = bdaddr_type; |
| |
| return queue_find(database->device_states, dev_state_match, &dev_info); |
| } |
| |
| static bool ccc_state_match(const void *a, const void *b) |
| { |
| const struct ccc_state *ccc = a; |
| uint16_t handle = PTR_TO_UINT(b); |
| |
| return ccc->handle == handle; |
| } |
| |
| static struct ccc_state *find_ccc_state(struct device_state *dev_state, |
| uint16_t handle) |
| { |
| return queue_find(dev_state->ccc_states, ccc_state_match, |
| UINT_TO_PTR(handle)); |
| } |
| |
| static struct device_state *device_state_create(struct btd_gatt_database *db, |
| const bdaddr_t *bdaddr, |
| uint8_t bdaddr_type) |
| { |
| struct device_state *dev_state; |
| |
| dev_state = new0(struct device_state, 1); |
| dev_state->db = db; |
| dev_state->ccc_states = queue_new(); |
| bacpy(&dev_state->bdaddr, bdaddr); |
| dev_state->bdaddr_type = bdaddr_type; |
| |
| return dev_state; |
| } |
| |
| static void device_state_free(void *data) |
| { |
| struct device_state *state = data; |
| |
| queue_destroy(state->ccc_states, free); |
| |
| if (state->pending) { |
| free(state->pending->value); |
| free(state->pending); |
| } |
| |
| free(state); |
| } |
| |
| static void clear_ccc_state(void *data, void *user_data) |
| { |
| struct ccc_state *ccc = data; |
| struct btd_gatt_database *db = user_data; |
| struct ccc_cb_data *ccc_cb; |
| |
| if (!ccc->value) |
| return; |
| |
| ccc_cb = queue_find(db->ccc_callbacks, ccc_cb_match_handle, |
| UINT_TO_PTR(ccc->handle)); |
| if (!ccc_cb) |
| return; |
| |
| if (ccc_cb->callback) |
| ccc_cb->callback(NULL, ccc_cb->user_data); |
| } |
| |
| static void att_disconnected(int err, void *user_data) |
| { |
| struct device_state *state = user_data; |
| struct btd_device *device; |
| |
| DBG(""); |
| |
| state->disc_id = 0; |
| state->out_of_sync = false; |
| |
| device = btd_adapter_find_device(state->db->adapter, &state->bdaddr, |
| state->bdaddr_type); |
| if (!device) |
| goto remove; |
| |
| if (device_is_bonded(device, state->bdaddr_type)) { |
| struct ccc_state *ccc; |
| uint16_t handle; |
| |
| handle = gatt_db_attribute_get_handle(state->db->svc_chngd_ccc); |
| |
| ccc = find_ccc_state(state, handle); |
| if (ccc && ccc->value) |
| device_store_svc_chng_ccc(device, state->bdaddr_type, |
| ccc->value); |
| |
| return; |
| } |
| |
| remove: |
| /* Remove device state if device no longer exists or is not paired */ |
| if (queue_remove(state->db->device_states, state)) { |
| queue_foreach(state->ccc_states, clear_ccc_state, state->db); |
| device_state_free(state); |
| } |
| } |
| |
| static bool get_dst_info(struct bt_att *att, bdaddr_t *dst, uint8_t *dst_type) |
| { |
| GIOChannel *io = NULL; |
| GError *gerr = NULL; |
| int fd; |
| |
| fd = bt_att_get_fd(att); |
| if (fd < 0) |
| return false; |
| |
| io = g_io_channel_unix_new(fd); |
| if (!io) |
| return false; |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_DEST_BDADDR, dst, |
| BT_IO_OPT_DEST_TYPE, dst_type, |
| BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("gatt: bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| g_io_channel_unref(io); |
| return false; |
| } |
| |
| g_io_channel_unref(io); |
| return true; |
| } |
| |
| static struct device_state * |
| find_device_state_by_att(struct btd_gatt_database *database, struct bt_att *att) |
| { |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| |
| if (!get_dst_info(att, &bdaddr, &bdaddr_type)) |
| return NULL; |
| |
| return find_device_state(database, &bdaddr, bdaddr_type); |
| } |
| |
| static struct device_state *get_device_state(struct btd_gatt_database *database, |
| struct bt_att *att) |
| { |
| struct device_state *dev_state; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| |
| if (!get_dst_info(att, &bdaddr, &bdaddr_type)) |
| return NULL; |
| |
| /* |
| * Find and return a device state. If a matching state doesn't exist, |
| * then create a new one. |
| */ |
| dev_state = find_device_state(database, &bdaddr, bdaddr_type); |
| if (dev_state) |
| goto done; |
| |
| dev_state = device_state_create(database, &bdaddr, bdaddr_type); |
| |
| queue_push_tail(database->device_states, dev_state); |
| |
| done: |
| if (!dev_state->disc_id) |
| dev_state->disc_id = bt_att_register_disconnect(att, |
| att_disconnected, |
| dev_state, NULL); |
| |
| return dev_state; |
| } |
| |
| static struct ccc_state *get_ccc_state(struct btd_gatt_database *database, |
| struct bt_att *att, uint16_t handle) |
| { |
| struct device_state *dev_state; |
| struct ccc_state *ccc; |
| |
| dev_state = get_device_state(database, att); |
| if (!dev_state) |
| return NULL; |
| |
| ccc = find_ccc_state(dev_state, handle); |
| if (ccc) |
| return ccc; |
| |
| ccc = new0(struct ccc_state, 1); |
| ccc->handle = handle; |
| queue_push_tail(dev_state->ccc_states, ccc); |
| |
| return ccc; |
| } |
| |
| static void cancel_pending_read(void *data) |
| { |
| struct pending_op *op = data; |
| |
| gatt_db_attribute_read_result(op->attrib, op->id, |
| BT_ATT_ERROR_REQUEST_NOT_SUPPORTED, |
| NULL, 0); |
| op->owner_queue = NULL; |
| } |
| |
| static void cancel_pending_write(void *data) |
| { |
| struct pending_op *op = data; |
| |
| gatt_db_attribute_write_result(op->attrib, op->id, |
| BT_ATT_ERROR_REQUEST_NOT_SUPPORTED); |
| op->owner_queue = NULL; |
| } |
| |
| static void client_io_free(void *data) |
| { |
| struct client_io *client = data; |
| |
| bt_att_unregister_disconnect(client->att, client->disconn_id); |
| bt_att_unref(client->att); |
| io_destroy(client->io); |
| free(client); |
| } |
| |
| static void chrc_free(void *data) |
| { |
| struct external_chrc *chrc = data; |
| |
| queue_destroy(chrc->write_ios, client_io_free); |
| queue_destroy(chrc->notify_ios, client_io_free); |
| |
| queue_destroy(chrc->pending_reads, cancel_pending_read); |
| queue_destroy(chrc->pending_writes, cancel_pending_write); |
| |
| g_free(chrc->path); |
| |
| g_dbus_proxy_set_property_watch(chrc->proxy, NULL, NULL); |
| g_dbus_proxy_unref(chrc->proxy); |
| |
| free(chrc); |
| } |
| |
| static void desc_free(void *data) |
| { |
| struct external_desc *desc = data; |
| |
| queue_destroy(desc->pending_reads, cancel_pending_read); |
| queue_destroy(desc->pending_writes, cancel_pending_write); |
| |
| g_dbus_proxy_unref(desc->proxy); |
| g_free(desc->chrc_path); |
| |
| free(desc); |
| } |
| |
| static void inc_free(void *data) |
| { |
| struct external_desc *inc = data; |
| |
| free(inc); |
| } |
| |
| static void service_free(void *data) |
| { |
| struct external_service *service = data; |
| |
| queue_destroy(service->chrcs, chrc_free); |
| queue_destroy(service->descs, desc_free); |
| queue_destroy(service->includes, inc_free); |
| |
| if (service->attrib) |
| gatt_db_remove_service(service->app->database->db, |
| service->attrib); |
| |
| if (service->app->client) |
| g_dbus_proxy_unref(service->proxy); |
| |
| g_free(service->path); |
| |
| free(service); |
| } |
| |
| static void profile_remove(void *data) |
| { |
| struct btd_profile *p = data; |
| |
| DBG("Removed \"%s\"", p->name); |
| |
| btd_adapter_foreach(adapter_remove_profile, p); |
| btd_profile_unregister(p); |
| |
| g_free((void *) p->name); |
| g_free((void *) p->remote_uuid); |
| |
| free(p); |
| } |
| |
| static void profile_release(struct external_profile *profile) |
| { |
| DBG("Releasing \"%s\"", profile->app->owner); |
| |
| g_dbus_proxy_method_call(profile->proxy, "Release", NULL, NULL, NULL, |
| NULL); |
| } |
| |
| static void profile_free(void *data) |
| { |
| struct external_profile *profile = data; |
| |
| queue_destroy(profile->profiles, profile_remove); |
| |
| profile_release(profile); |
| |
| g_dbus_proxy_unref(profile->proxy); |
| |
| free(profile); |
| } |
| |
| static void app_free(void *data) |
| { |
| struct gatt_app *app = data; |
| |
| queue_destroy(app->profiles, profile_free); |
| queue_destroy(app->services, service_free); |
| queue_destroy(app->proxies, NULL); |
| |
| if (app->client) { |
| g_dbus_client_set_disconnect_watch(app->client, NULL, NULL); |
| g_dbus_client_set_proxy_handlers(app->client, NULL, NULL, |
| NULL, NULL); |
| g_dbus_client_set_ready_watch(app->client, NULL, NULL); |
| g_dbus_client_unref(app->client); |
| } |
| |
| if (app->reg) |
| dbus_message_unref(app->reg); |
| |
| g_free(app->owner); |
| g_free(app->path); |
| |
| free(app); |
| } |
| |
| static void gatt_record_free(void *data) |
| { |
| struct gatt_record *rec = data; |
| |
| adapter_service_remove(rec->database->adapter, rec->handle); |
| free(rec); |
| } |
| |
| static void gatt_database_free(void *data) |
| { |
| struct btd_gatt_database *database = data; |
| |
| if (database->le_io) { |
| g_io_channel_shutdown(database->le_io, FALSE, NULL); |
| g_io_channel_unref(database->le_io); |
| } |
| |
| if (database->eatt_io) { |
| g_io_channel_shutdown(database->eatt_io, FALSE, NULL); |
| g_io_channel_unref(database->eatt_io); |
| } |
| |
| if (database->bredr_io) { |
| g_io_channel_shutdown(database->bredr_io, FALSE, NULL); |
| g_io_channel_unref(database->bredr_io); |
| } |
| |
| /* TODO: Persistently store CCC states before freeing them */ |
| gatt_db_unregister(database->db, database->db_id); |
| |
| queue_destroy(database->records, gatt_record_free); |
| queue_destroy(database->device_states, device_state_free); |
| queue_destroy(database->apps, app_free); |
| queue_destroy(database->profiles, profile_free); |
| queue_destroy(database->ccc_callbacks, ccc_cb_free); |
| database->device_states = NULL; |
| database->ccc_callbacks = NULL; |
| |
| gatt_db_unref(database->db); |
| |
| btd_adapter_unref(database->adapter); |
| free(database); |
| } |
| |
| static void connect_cb(GIOChannel *io, GError *gerr, gpointer user_data) |
| { |
| struct btd_adapter *adapter; |
| struct btd_device *device; |
| uint8_t dst_type; |
| bdaddr_t src, dst; |
| |
| if (gerr) { |
| error("%s", gerr->message); |
| return; |
| } |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_DEST_TYPE, &dst_type, |
| BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| return; |
| } |
| |
| DBG("New incoming %s ATT connection", dst_type == BDADDR_BREDR ? |
| "BR/EDR" : "LE"); |
| |
| adapter = adapter_find(&src); |
| if (!adapter) |
| return; |
| |
| device = btd_adapter_get_device(adapter, &dst, dst_type); |
| if (!device) |
| return; |
| |
| device_attach_att(device, io); |
| } |
| |
| static void gap_device_name_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| uint8_t error = 0; |
| size_t len = 0; |
| const uint8_t *value = NULL; |
| const char *device_name; |
| |
| DBG("GAP Device Name read request\n"); |
| |
| device_name = btd_adapter_get_name(database->adapter); |
| len = strlen(device_name); |
| |
| if (offset > len) { |
| error = BT_ATT_ERROR_INVALID_OFFSET; |
| goto done; |
| } |
| |
| len -= offset; |
| value = len ? (const uint8_t *) &device_name[offset] : NULL; |
| |
| done: |
| gatt_db_attribute_read_result(attrib, id, error, value, len); |
| } |
| |
| static void gap_appearance_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| uint8_t error = 0; |
| size_t len = 2; |
| const uint8_t *value = NULL; |
| uint8_t appearance[2]; |
| uint32_t dev_class; |
| |
| DBG("GAP Appearance read request\n"); |
| |
| dev_class = btd_adapter_get_class(database->adapter); |
| |
| appearance[0] = dev_class & 0x00ff; |
| appearance[1] = (dev_class >> 8) & 0x001f; |
| |
| len -= offset; |
| value = len ? &appearance[offset] : NULL; |
| |
| gatt_db_attribute_read_result(attrib, id, error, value, len); |
| } |
| |
| static void gap_car_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| uint8_t value = 0x00; |
| |
| DBG("GAP Central Address Resolution read request\n"); |
| |
| if (btd_opts.defaults.le.addr_resolution) { |
| struct btd_device *device; |
| |
| device = btd_adapter_find_device_by_fd(bt_att_get_fd(att)); |
| if (device) |
| value = btd_device_flags_enabled(device, |
| DEVICE_FLAG_ADDRESS_RESOLUTION); |
| } |
| |
| gatt_db_attribute_read_result(attrib, id, 0, &value, sizeof(value)); |
| } |
| |
| static sdp_record_t *record_new(uuid_t *uuid, uint16_t start, uint16_t end) |
| { |
| sdp_list_t *svclass_id, *apseq, *proto[2], *root, *aproto; |
| uuid_t root_uuid, proto_uuid, l2cap; |
| sdp_record_t *record; |
| sdp_data_t *psm, *sh, *eh; |
| uint16_t lp = BT_ATT_PSM; |
| |
| if (uuid == NULL) |
| return NULL; |
| |
| if (start > end) |
| return NULL; |
| |
| record = sdp_record_alloc(); |
| if (record == NULL) |
| return NULL; |
| |
| sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); |
| root = sdp_list_append(NULL, &root_uuid); |
| sdp_set_browse_groups(record, root); |
| sdp_list_free(root, NULL); |
| |
| svclass_id = sdp_list_append(NULL, uuid); |
| sdp_set_service_classes(record, svclass_id); |
| sdp_list_free(svclass_id, NULL); |
| |
| sdp_uuid16_create(&l2cap, L2CAP_UUID); |
| proto[0] = sdp_list_append(NULL, &l2cap); |
| psm = sdp_data_alloc(SDP_UINT16, &lp); |
| proto[0] = sdp_list_append(proto[0], psm); |
| apseq = sdp_list_append(NULL, proto[0]); |
| |
| sdp_uuid16_create(&proto_uuid, ATT_UUID); |
| proto[1] = sdp_list_append(NULL, &proto_uuid); |
| sh = sdp_data_alloc(SDP_UINT16, &start); |
| proto[1] = sdp_list_append(proto[1], sh); |
| eh = sdp_data_alloc(SDP_UINT16, &end); |
| proto[1] = sdp_list_append(proto[1], eh); |
| apseq = sdp_list_append(apseq, proto[1]); |
| |
| aproto = sdp_list_append(NULL, apseq); |
| sdp_set_access_protos(record, aproto); |
| |
| sdp_data_free(psm); |
| sdp_data_free(sh); |
| sdp_data_free(eh); |
| sdp_list_free(proto[0], NULL); |
| sdp_list_free(proto[1], NULL); |
| sdp_list_free(apseq, NULL); |
| sdp_list_free(aproto, NULL); |
| |
| return record; |
| } |
| |
| static void database_add_record(struct btd_gatt_database *database, |
| struct gatt_db_attribute *attr) |
| { |
| struct gatt_record *rec; |
| sdp_record_t *record; |
| uint16_t start, end; |
| uuid_t svc, gap_uuid; |
| bt_uuid_t uuid; |
| const char *name = NULL; |
| char uuidstr[MAX_LEN_UUID_STR]; |
| |
| gatt_db_attribute_get_service_uuid(attr, &uuid); |
| |
| switch (uuid.type) { |
| case BT_UUID16: |
| name = bt_uuid16_to_str(uuid.value.u16); |
| sdp_uuid16_create(&svc, uuid.value.u16); |
| break; |
| case BT_UUID32: |
| name = bt_uuid32_to_str(uuid.value.u32); |
| sdp_uuid32_create(&svc, uuid.value.u32); |
| break; |
| case BT_UUID128: |
| bt_uuid_to_string(&uuid, uuidstr, sizeof(uuidstr)); |
| name = bt_uuidstr_to_str(uuidstr); |
| sdp_uuid128_create(&svc, (void *) &uuid.value.u128); |
| break; |
| case BT_UUID_UNSPEC: |
| return; |
| } |
| |
| gatt_db_attribute_get_service_handles(attr, &start, &end); |
| |
| record = record_new(&svc, start, end); |
| if (!record) |
| return; |
| |
| if (name != NULL) |
| sdp_set_info_attr(record, name, "BlueZ", NULL); |
| |
| sdp_uuid16_create(&gap_uuid, UUID_GAP); |
| if (sdp_uuid_cmp(&svc, &gap_uuid) == 0) { |
| sdp_set_url_attr(record, "http://www.bluez.org/", |
| "http://www.bluez.org/", |
| "http://www.bluez.org/"); |
| } |
| |
| if (adapter_service_add(database->adapter, record) < 0) { |
| sdp_record_free(record); |
| return; |
| } |
| |
| rec = new0(struct gatt_record, 1); |
| rec->database = database; |
| rec->handle = record->handle; |
| rec->attr = attr; |
| queue_push_tail(database->records, rec); |
| } |
| |
| static void populate_gap_service(struct btd_gatt_database *database) |
| { |
| bt_uuid_t uuid; |
| struct gatt_db_attribute *service, *attrib; |
| bool ll_privacy = btd_adapter_has_settings(database->adapter, |
| MGMT_SETTING_LL_PRIVACY); |
| |
| /* Add the GAP service */ |
| bt_uuid16_create(&uuid, UUID_GAP); |
| service = gatt_db_add_service(database->db, &uuid, true, |
| ll_privacy ? 7 : 5); |
| |
| /* |
| * Device Name characteristic. |
| */ |
| bt_uuid16_create(&uuid, GATT_CHARAC_DEVICE_NAME); |
| gatt_db_service_add_characteristic(service, &uuid, BT_ATT_PERM_READ, |
| BT_GATT_CHRC_PROP_READ, |
| gap_device_name_read_cb, |
| NULL, database); |
| |
| /* |
| * Device Appearance characteristic. |
| */ |
| bt_uuid16_create(&uuid, GATT_CHARAC_APPEARANCE); |
| attrib = gatt_db_service_add_characteristic(service, &uuid, |
| BT_ATT_PERM_READ, |
| BT_GATT_CHRC_PROP_READ, |
| gap_appearance_read_cb, |
| NULL, database); |
| gatt_db_attribute_set_fixed_length(attrib, 2); |
| |
| /* Only enable Central Address Resolution if LL Privacy is supported */ |
| if (ll_privacy) { |
| /* |
| * Central Address Resolution characteristic. |
| */ |
| bt_uuid16_create(&uuid, GATT_CHARAC_CAR); |
| attrib = gatt_db_service_add_characteristic(service, &uuid, |
| BT_ATT_PERM_READ, |
| BT_GATT_CHRC_PROP_READ, |
| gap_car_read_cb, |
| NULL, database); |
| } |
| |
| gatt_db_attribute_set_fixed_length(attrib, 1); |
| |
| gatt_db_service_set_active(service, true); |
| |
| database_add_record(database, service); |
| } |
| |
| static void gatt_ccc_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct ccc_state *ccc; |
| uint16_t handle; |
| uint8_t ecode = 0; |
| const uint8_t *value = NULL; |
| size_t len = 0; |
| |
| handle = gatt_db_attribute_get_handle(attrib); |
| |
| DBG("CCC read called for handle: 0x%04x", handle); |
| |
| ccc = get_ccc_state(database, att, handle); |
| if (!ccc) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| len = sizeof(ccc->value); |
| value = (void *) &ccc->value; |
| |
| done: |
| gatt_db_attribute_read_result(attrib, id, ecode, value, len); |
| } |
| |
| static struct btd_device *att_get_device(struct bt_att *att) |
| { |
| GIOChannel *io = NULL; |
| GError *gerr = NULL; |
| bdaddr_t src, dst; |
| uint8_t dst_type; |
| struct btd_adapter *adapter; |
| |
| io = g_io_channel_unix_new(bt_att_get_fd(att)); |
| if (!io) |
| return NULL; |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_DEST_TYPE, &dst_type, |
| BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| g_io_channel_unref(io); |
| return NULL; |
| } |
| |
| g_io_channel_unref(io); |
| |
| adapter = adapter_find(&src); |
| if (!adapter) { |
| error("Unable to find adapter object"); |
| return NULL; |
| } |
| |
| return btd_adapter_find_device(adapter, &dst, dst_type); |
| } |
| |
| |
| static void pending_op_free(void *data) |
| { |
| struct pending_op *op = data; |
| |
| if (op->owner_queue) |
| queue_remove(op->owner_queue, op); |
| |
| bt_att_unregister_disconnect(op->att, op->disconn_id); |
| bt_att_unref(op->att); |
| free(op); |
| } |
| |
| static void pending_disconnect_cb(int err, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| |
| op->owner_queue = NULL; |
| } |
| |
| static struct pending_op *pending_ccc_new(struct bt_att *att, |
| struct gatt_db_attribute *attrib, |
| uint16_t value, |
| uint8_t link_type) |
| { |
| struct pending_op *op; |
| struct btd_device *device; |
| |
| device = att_get_device(att); |
| if (!device) { |
| error("Unable to find device object"); |
| return NULL; |
| } |
| |
| op = new0(struct pending_op, 1); |
| |
| op->data.iov_base = UINT_TO_PTR(value); |
| op->data.iov_len = sizeof(value); |
| |
| op->att = bt_att_ref(att); |
| op->attrib = attrib; |
| op->link_type = link_type; |
| |
| op->disconn_id = bt_att_register_disconnect(att, |
| pending_disconnect_cb, |
| op, |
| NULL); |
| |
| return op; |
| } |
| |
| static void gatt_ccc_write_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| const uint8_t *value, size_t len, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct ccc_state *ccc; |
| struct ccc_cb_data *ccc_cb; |
| uint16_t handle, val; |
| uint8_t ecode = 0; |
| |
| handle = gatt_db_attribute_get_handle(attrib); |
| |
| DBG("CCC write called for handle: 0x%04x", handle); |
| |
| if (!value || len > 2) { |
| ecode = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN; |
| goto done; |
| } |
| |
| if (offset > 2) { |
| ecode = BT_ATT_ERROR_INVALID_OFFSET; |
| goto done; |
| } |
| |
| ccc = get_ccc_state(database, att, handle); |
| if (!ccc) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| if (len == 1) |
| val = *value; |
| else |
| val = get_le16(value); |
| |
| /* If value is identical, then just succeed */ |
| if (val == ccc->value) |
| goto done; |
| |
| ccc_cb = queue_find(database->ccc_callbacks, ccc_cb_match_handle, |
| UINT_TO_PTR(gatt_db_attribute_get_handle(attrib))); |
| if (ccc_cb) { |
| struct pending_op *op; |
| |
| op = pending_ccc_new(att, attrib, val, |
| bt_att_get_link_type(att)); |
| if (!op) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| ecode = ccc_cb->callback(op, ccc_cb->user_data); |
| if (ecode) |
| pending_op_free(op); |
| } |
| |
| if (!ecode) |
| ccc->value = val; |
| |
| done: |
| gatt_db_attribute_write_result(attrib, id, ecode); |
| } |
| |
| static void ccc_add_cb(struct btd_gatt_database *database, |
| struct gatt_db_attribute *ccc, |
| btd_gatt_database_ccc_write_t callback, |
| void *user_data, btd_gatt_database_destroy_t destroy) |
| { |
| struct ccc_cb_data *ccc_cb; |
| |
| ccc_cb = new0(struct ccc_cb_data, 1); |
| ccc_cb->handle = gatt_db_attribute_get_handle(ccc); |
| ccc_cb->callback = callback; |
| ccc_cb->destroy = destroy; |
| ccc_cb->user_data = user_data; |
| |
| queue_push_tail(database->ccc_callbacks, ccc_cb); |
| } |
| |
| static struct gatt_db_attribute * |
| service_add_ccc(struct gatt_db_attribute *service, |
| struct btd_gatt_database *database, |
| btd_gatt_database_ccc_write_t write_callback, |
| void *user_data, uint32_t perm, |
| btd_gatt_database_destroy_t destroy) |
| { |
| struct gatt_db_attribute *ccc; |
| |
| ccc = gatt_db_service_add_ccc(service, perm); |
| if (!ccc) |
| return ccc; |
| |
| /* Only add ccc_cb if callback is set */ |
| if (write_callback) |
| ccc_add_cb(database, ccc, write_callback, user_data, destroy); |
| |
| return ccc; |
| } |
| |
| static void cli_feat_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct device_state *state; |
| uint8_t ecode = 0; |
| const uint8_t *value = NULL; |
| size_t len = 0; |
| |
| DBG("Client Features read"); |
| |
| state = get_device_state(database, att); |
| if (!state) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| len = sizeof(state->cli_feat) - offset; |
| value = len ? &state->cli_feat[offset] : NULL; |
| |
| done: |
| gatt_db_attribute_read_result(attrib, id, ecode, value, len); |
| } |
| |
| static void cli_feat_write_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| const uint8_t *value, size_t len, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct device_state *state; |
| uint8_t bits[] = { BT_GATT_CHRC_CLI_FEAT_ROBUST_CACHING, |
| BT_GATT_CHRC_CLI_FEAT_EATT, |
| BT_GATT_CHRC_CLI_FEAT_NFY_MULTI }; |
| uint8_t ecode = 0; |
| unsigned int i; |
| |
| DBG("Client Features write"); |
| |
| state = get_device_state(database, att); |
| if (!state) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| if (!value || !len) { |
| ecode = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN; |
| goto done; |
| } |
| |
| for (i = 0; i < sizeof(bits); i++) { |
| /* A client shall never clear a bit it has set */ |
| if (state->cli_feat[0] & (1 << i) && !(value[0] & (1 << i))) { |
| ecode = BT_ATT_ERROR_VALUE_NOT_ALLOWED; |
| goto done; |
| } |
| } |
| |
| /* Shall we reallocate the feat array if bigger? */ |
| len = MIN(sizeof(state->cli_feat), len); |
| while (len) { |
| state->cli_feat[len - 1] |= value[len - 1]; |
| len--; |
| } |
| |
| state->cli_feat[0] &= ((1 << sizeof(bits)) - 1); |
| state->change_aware = true; |
| |
| done: |
| gatt_db_attribute_write_result(attrib, id, ecode); |
| } |
| |
| static void db_hash_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| const uint8_t *hash; |
| struct device_state *state; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| |
| DBG("Database Hash read"); |
| |
| hash = gatt_db_get_hash(database->db); |
| |
| gatt_db_attribute_read_result(attrib, id, 0, hash, 16); |
| |
| if (!get_dst_info(att, &bdaddr, &bdaddr_type)) |
| return; |
| |
| state = find_device_state(database, &bdaddr, bdaddr_type); |
| if (state) |
| state->change_aware = true; |
| } |
| |
| static void server_feat_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct device_state *state; |
| uint8_t ecode = 0; |
| uint8_t value = 0; |
| |
| state = get_device_state(database, att); |
| if (!state) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| if (btd_opts.gatt_channels > 1) |
| value |= BT_GATT_CHRC_SERVER_FEAT_EATT; |
| |
| done: |
| gatt_db_attribute_read_result(attrib, id, ecode, &value, sizeof(value)); |
| } |
| |
| static void populate_gatt_service(struct btd_gatt_database *database) |
| { |
| bt_uuid_t uuid; |
| struct gatt_db_attribute *service; |
| |
| /* Add the GATT service */ |
| bt_uuid16_create(&uuid, UUID_GATT); |
| service = gatt_db_add_service(database->db, &uuid, true, 10); |
| |
| bt_uuid16_create(&uuid, GATT_CHARAC_SERVICE_CHANGED); |
| database->svc_chngd = gatt_db_service_add_characteristic(service, &uuid, |
| BT_ATT_PERM_NONE, BT_GATT_CHRC_PROP_INDICATE, |
| NULL, NULL, database); |
| |
| database->svc_chngd_ccc = service_add_ccc(service, database, NULL, NULL, |
| BT_ATT_PERM_READ | |
| BT_ATT_PERM_WRITE, NULL); |
| |
| bt_uuid16_create(&uuid, GATT_CHARAC_CLI_FEAT); |
| database->cli_feat = gatt_db_service_add_characteristic(service, |
| &uuid, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE, |
| BT_GATT_CHRC_PROP_READ | |
| BT_GATT_CHRC_PROP_WRITE, |
| cli_feat_read_cb, cli_feat_write_cb, |
| database); |
| |
| gatt_db_attribute_set_fixed_length(database->cli_feat, CLI_FEAT_SIZE); |
| |
| /* Only expose database hash chrc if supported */ |
| if (gatt_db_hash_support(database->db)) { |
| bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); |
| database->db_hash = gatt_db_service_add_characteristic(service, |
| &uuid, BT_ATT_PERM_READ, BT_GATT_CHRC_PROP_READ, |
| db_hash_read_cb, NULL, database); |
| gatt_db_attribute_set_fixed_length(database->db_hash, 16); |
| } |
| |
| /* Only enable EATT if there is a socket listening */ |
| if (database->eatt_io) { |
| bt_uuid16_create(&uuid, GATT_CHARAC_SERVER_FEAT); |
| database->eatt = gatt_db_service_add_characteristic(service, |
| &uuid, BT_ATT_PERM_READ, BT_GATT_CHRC_PROP_READ, |
| server_feat_read_cb, NULL, database); |
| gatt_db_attribute_set_fixed_length(database->eatt, 1); |
| } |
| |
| gatt_db_service_set_active(service, true); |
| |
| database_add_record(database, service); |
| } |
| |
| static void device_info_read_pnp_id_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| uint8_t pdu[7]; |
| |
| pdu[0] = btd_opts.did_source; |
| put_le16(btd_opts.did_vendor, &pdu[1]); |
| put_le16(btd_opts.did_product, &pdu[3]); |
| put_le16(btd_opts.did_version, &pdu[5]); |
| |
| gatt_db_attribute_read_result(attrib, id, 0, pdu, sizeof(pdu)); |
| } |
| |
| static void populate_devinfo_service(struct btd_gatt_database *database) |
| { |
| struct gatt_db_attribute *service; |
| struct gatt_db_attribute *attrib; |
| bt_uuid_t uuid; |
| |
| if (!btd_opts.did_source) |
| return; |
| |
| bt_uuid16_create(&uuid, UUID_DIS); |
| service = gatt_db_add_service(database->db, &uuid, true, 3); |
| |
| bt_uuid16_create(&uuid, GATT_CHARAC_PNP_ID); |
| attrib = gatt_db_service_add_characteristic(service, &uuid, |
| BT_ATT_PERM_READ, |
| BT_GATT_CHRC_PROP_READ, |
| device_info_read_pnp_id_cb, |
| NULL, database); |
| gatt_db_attribute_set_fixed_length(attrib, 7); |
| |
| gatt_db_service_set_active(service, true); |
| |
| database_add_record(database, service); |
| } |
| |
| static void conf_cb(void *user_data) |
| { |
| GDBusProxy *proxy = user_data; |
| DBG("GATT server received confirmation"); |
| |
| if (proxy != NULL) |
| { |
| g_dbus_proxy_method_call(proxy, "Confirm", NULL, NULL, NULL, NULL); |
| } |
| } |
| |
| static void service_changed_conf(void *user_data) |
| { |
| struct device_state *state = user_data; |
| |
| DBG(""); |
| |
| if (!state) |
| return; |
| |
| state->change_aware = true; |
| } |
| |
| static void state_set_pending(struct device_state *state, struct notify *notify) |
| { |
| uint16_t start, end, old_start, old_end; |
| |
| /* Cache this only for Service Changed */ |
| if (notify->conf != service_changed_conf) |
| return; |
| |
| if (state->pending) { |
| old_start = get_le16(state->pending->value); |
| old_end = get_le16(state->pending->value + 2); |
| |
| start = get_le16(notify->value); |
| end = get_le16(notify->value + 2); |
| |
| if (start < old_start) |
| put_le16(start, state->pending->value); |
| |
| if (end > old_end) |
| put_le16(end, state->pending->value + 2); |
| |
| return; |
| } |
| |
| /* Copy notify contents to pending */ |
| state->pending = new0(struct notify, 1); |
| memcpy(state->pending, notify, sizeof(*notify)); |
| state->pending->value = malloc(notify->len); |
| memcpy(state->pending->value, notify->value, notify->len); |
| } |
| |
| static void send_notification_to_device(void *data, void *user_data) |
| { |
| struct device_state *device_state = data; |
| struct notify *notify = user_data; |
| struct ccc_state *ccc; |
| struct btd_device *device; |
| struct bt_gatt_server *server; |
| |
| if (notify->conf == service_changed_conf) { |
| if (device_state->cli_feat[0] & |
| BT_GATT_CHRC_CLI_FEAT_ROBUST_CACHING) { |
| device_state->change_aware = false; |
| notify->user_data = device_state; |
| } |
| } |
| |
| ccc = find_ccc_state(device_state, notify->ccc_handle); |
| if (!ccc || !(ccc->value & 0x0003)) |
| return; |
| |
| device = btd_adapter_find_device(notify->database->adapter, |
| &device_state->bdaddr, |
| device_state->bdaddr_type); |
| if (!device) { |
| /* If ATT has not disconnect yet don't remove the state as it |
| * will eventually be removed when att_disconnected is called. |
| */ |
| if (device_state->disc_id) |
| return; |
| goto remove; |
| } |
| |
| server = btd_device_get_gatt_server(device); |
| if (!server) { |
| if (!device_is_bonded(device, device_state->bdaddr_type)) |
| goto remove; |
| state_set_pending(device_state, notify); |
| return; |
| } |
| |
| /* |
| * TODO: If the device is not connected but bonded, send the |
| * notification/indication when it becomes connected. |
| */ |
| if (!(ccc->value & 0x0002)) { |
| DBG("GATT server sending notification"); |
| bt_gatt_server_send_notification(server, |
| notify->handle, notify->value, |
| notify->len, device_state->cli_feat[0] & |
| BT_GATT_CHRC_CLI_FEAT_NFY_MULTI); |
| return; |
| } |
| |
| DBG("GATT server sending indication"); |
| bt_gatt_server_send_indication(server, notify->handle, notify->value, |
| notify->len, notify->conf, |
| notify->user_data, NULL); |
| |
| return; |
| |
| remove: |
| /* Remove device state if device no longer exists or is not paired */ |
| if (queue_remove(notify->database->device_states, device_state)) { |
| queue_foreach(device_state->ccc_states, clear_ccc_state, |
| notify->database); |
| device_state_free(device_state); |
| } |
| } |
| |
| static void gatt_notify_cb(struct gatt_db_attribute *attrib, |
| struct gatt_db_attribute *ccc, |
| const uint8_t *value, size_t len, |
| struct bt_att *att, void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct notify notify; |
| |
| memset(¬ify, 0, sizeof(notify)); |
| |
| notify.database = database; |
| notify.handle = gatt_db_attribute_get_handle(attrib); |
| notify.ccc_handle = gatt_db_attribute_get_handle(ccc); |
| notify.value = (void *) value; |
| notify.len = len; |
| |
| if (attrib == database->svc_chngd) |
| notify.conf = service_changed_conf; |
| |
| /* If a specific att is provided notify only to that device */ |
| if (att) { |
| struct device_state *state; |
| |
| state = find_device_state_by_att(database, att); |
| if (!state) |
| return; |
| |
| send_notification_to_device(state, ¬ify); |
| } else |
| queue_foreach(database->device_states, |
| send_notification_to_device, ¬ify); |
| } |
| |
| static void register_core_services(struct btd_gatt_database *database) |
| { |
| gatt_db_ccc_register(database->db, gatt_ccc_read_cb, gatt_ccc_write_cb, |
| gatt_notify_cb, database); |
| |
| populate_gap_service(database); |
| populate_gatt_service(database); |
| populate_devinfo_service(database); |
| } |
| |
| static void send_notification_to_devices(struct btd_gatt_database *database, |
| uint16_t handle, uint8_t *value, |
| uint16_t len, uint16_t ccc_handle, |
| bt_gatt_server_conf_func_t conf, |
| void *user_data) |
| { |
| struct notify notify; |
| |
| memset(¬ify, 0, sizeof(notify)); |
| |
| notify.database = database; |
| notify.handle = handle; |
| notify.ccc_handle = ccc_handle; |
| notify.value = value; |
| notify.len = len; |
| notify.conf = conf; |
| notify.user_data = user_data; |
| |
| queue_foreach(database->device_states, send_notification_to_device, |
| ¬ify); |
| } |
| |
| static void send_service_changed(struct btd_gatt_database *database, |
| struct gatt_db_attribute *attrib) |
| { |
| uint16_t start, end; |
| uint8_t value[4]; |
| uint16_t handle, ccc_handle; |
| |
| if (!gatt_db_attribute_get_service_handles(attrib, &start, &end)) { |
| error("Failed to obtain changed service handles"); |
| return; |
| } |
| |
| handle = gatt_db_attribute_get_handle(database->svc_chngd); |
| ccc_handle = gatt_db_attribute_get_handle(database->svc_chngd_ccc); |
| |
| if (!handle || !ccc_handle) { |
| error("Failed to obtain handles for \"Service Changed\"" |
| " characteristic"); |
| return; |
| } |
| |
| put_le16(start, value); |
| put_le16(end, value + 2); |
| |
| if (!gatt_db_attribute_notify(database->svc_chngd, value, sizeof(value), |
| NULL)) |
| error("Failed to notify Service Changed"); |
| } |
| |
| static void database_store(struct btd_gatt_database *database) |
| { |
| char filename[PATH_MAX]; |
| |
| create_filename(filename, PATH_MAX, "/%s/attributes", |
| btd_adapter_get_storage_dir(database->adapter)); |
| create_file(filename, 0600); |
| |
| btd_settings_gatt_db_store(database->db, filename); |
| } |
| |
| static void gatt_db_service_added(struct gatt_db_attribute *attrib, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| |
| DBG("GATT Service added to local database"); |
| |
| database_add_record(database, attrib); |
| |
| send_service_changed(database, attrib); |
| |
| database_store(database); |
| } |
| |
| static bool ccc_match_service(const void *data, const void *match_data) |
| { |
| const struct ccc_state *ccc = data; |
| const struct gatt_db_attribute *attrib = match_data; |
| uint16_t start, end; |
| |
| if (!gatt_db_attribute_get_service_handles(attrib, &start, &end)) |
| return false; |
| |
| return ccc->handle >= start && ccc->handle <= end; |
| } |
| |
| static void remove_device_ccc(void *data, void *user_data) |
| { |
| struct device_state *state = data; |
| |
| queue_remove_all(state->ccc_states, ccc_match_service, user_data, free); |
| } |
| |
| static bool match_gatt_record(const void *data, const void *user_data) |
| { |
| const struct gatt_record *rec = data; |
| const struct gatt_db_attribute *attr = user_data; |
| |
| return (rec->attr == attr); |
| } |
| |
| static void gatt_db_service_removed(struct gatt_db_attribute *attrib, |
| void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct gatt_record *rec; |
| |
| DBG("Local GATT service removed"); |
| |
| rec = queue_remove_if(database->records, match_gatt_record, attrib); |
| if (rec) |
| gatt_record_free(rec); |
| |
| send_service_changed(database, attrib); |
| |
| queue_foreach(database->device_states, remove_device_ccc, attrib); |
| queue_remove_all(database->ccc_callbacks, ccc_cb_match_service, attrib, |
| ccc_cb_free); |
| } |
| |
| struct svc_match_data { |
| const char *path; |
| const char *sender; |
| }; |
| |
| static bool match_app(const void *a, const void *b) |
| { |
| const struct gatt_app *app = a; |
| const struct svc_match_data *data = b; |
| |
| return g_strcmp0(app->path, data->path) == 0 && |
| g_strcmp0(app->owner, data->sender) == 0; |
| } |
| |
| static gboolean app_free_idle_cb(void *data) |
| { |
| app_free(data); |
| |
| return FALSE; |
| } |
| |
| static void client_disconnect_cb(DBusConnection *conn, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| struct btd_gatt_database *database = app->database; |
| |
| DBG("Client disconnected"); |
| |
| if (queue_remove(database->apps, app)) |
| app_free(app); |
| } |
| |
| static void remove_app(void *data) |
| { |
| struct gatt_app *app = data; |
| |
| /* |
| * Set callback to NULL to avoid potential race condition |
| * when calling remove_app and GDBusClient unref. |
| */ |
| g_dbus_client_set_disconnect_watch(app->client, NULL, NULL); |
| |
| /* |
| * Set proxy handlers to NULL, so that this gets called only once when |
| * the first proxy that belongs to this service gets removed. |
| */ |
| g_dbus_client_set_proxy_handlers(app->client, NULL, NULL, NULL, NULL); |
| |
| |
| queue_remove(app->database->apps, app); |
| |
| /* |
| * Do not run in the same loop, this may be a disconnect |
| * watch call and GDBusClient should not be destroyed. |
| */ |
| g_idle_add(app_free_idle_cb, app); |
| } |
| |
| static bool match_service_by_path(const void *a, const void *b) |
| { |
| const struct external_service *service = a; |
| const char *path = b; |
| |
| return strcmp(service->path, path) == 0; |
| } |
| |
| static bool parse_path(GDBusProxy *proxy, const char *name, const char **path) |
| { |
| DBusMessageIter iter; |
| |
| if (!g_dbus_proxy_get_property(proxy, name, &iter)) |
| return false; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) |
| return false; |
| |
| dbus_message_iter_get_basic(&iter, path); |
| |
| return true; |
| } |
| |
| static bool incr_attr_count(struct external_service *service, uint16_t incr) |
| { |
| if (service->attr_cnt > UINT16_MAX - incr) |
| return false; |
| |
| service->attr_cnt += incr; |
| |
| return true; |
| } |
| |
| static bool parse_chrc_flags(DBusMessageIter *array, uint8_t *props, |
| uint8_t *ext_props, uint32_t *perm, |
| uint32_t *ccc_perm, |
| bool *req_prep_authorization) |
| { |
| const char *flag; |
| |
| if (!props || !ext_props || !perm || !ccc_perm) |
| return false; |
| |
| *props = 0; |
| *ext_props = 0; |
| *perm = 0; |
| *ccc_perm = 0; |
| |
| do { |
| if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_STRING) |
| return false; |
| |
| dbus_message_iter_get_basic(array, &flag); |
| |
| if (!strcmp("broadcast", flag)) |
| *props |= BT_GATT_CHRC_PROP_BROADCAST; |
| else if (!strcmp("read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *perm |= BT_ATT_PERM_READ; |
| } else if (!strcmp("write-without-response", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP; |
| *perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("notify", flag)) { |
| *props |= BT_GATT_CHRC_PROP_NOTIFY; |
| *ccc_perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("indicate", flag)) { |
| *props |= BT_GATT_CHRC_PROP_INDICATE; |
| *ccc_perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("authenticated-signed-writes", flag)) { |
| *props |= BT_GATT_CHRC_PROP_AUTH; |
| *perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("extended-properties", flag)) { |
| *props |= BT_GATT_CHRC_PROP_EXT_PROP; |
| } else if (!strcmp("reliable-write", flag)) { |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_RELIABLE_WRITE; |
| *perm |= BT_ATT_PERM_WRITE; |
| } else if (!strcmp("writable-auxiliaries", flag)) { |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_WRITABLE_AUX; |
| } else if (!strcmp("encrypt-read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_ENCRYPT; |
| } else if (!strcmp("encrypt-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_ENCRYPT; |
| } else if (!strcmp("encrypt-authenticated-read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_AUTHEN; |
| } else if (!strcmp("encrypt-authenticated-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_AUTHEN; |
| } else if (!strcmp("secure-read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_SECURE; |
| } else if (!strcmp("secure-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_SECURE; |
| } else if (!strcmp("authorize", flag)) { |
| *req_prep_authorization = true; |
| } else if (!strcmp("encrypt-notify", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_ENCRYPT; |
| *props |= BT_GATT_CHRC_PROP_NOTIFY; |
| } else if (!strcmp("encrypt-authenticated-notify", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_AUTHEN; |
| *props |= BT_GATT_CHRC_PROP_NOTIFY; |
| } else if (!strcmp("secure-notify", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_SECURE; |
| *props |= BT_GATT_CHRC_PROP_NOTIFY; |
| } else if (!strcmp("encrypt-indicate", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_ENCRYPT; |
| *props |= BT_GATT_CHRC_PROP_INDICATE; |
| } else if (!strcmp("encrypt-authenticated-indicate", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_AUTHEN; |
| *props |= BT_GATT_CHRC_PROP_INDICATE; |
| } else if (!strcmp("secure-indicate", flag)) { |
| *ccc_perm |= BT_ATT_PERM_WRITE_SECURE; |
| *props |= BT_GATT_CHRC_PROP_INDICATE; |
| } else { |
| error("Invalid characteristic flag: %s", flag); |
| return false; |
| } |
| } while (dbus_message_iter_next(array)); |
| |
| if (*ext_props) |
| *props |= BT_GATT_CHRC_PROP_EXT_PROP; |
| |
| return true; |
| } |
| |
| static bool parse_desc_flags(DBusMessageIter *array, uint32_t *perm, |
| bool *req_prep_authorization) |
| { |
| const char *flag; |
| |
| *perm = 0; |
| |
| do { |
| if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_STRING) |
| return false; |
| |
| dbus_message_iter_get_basic(array, &flag); |
| |
| if (!strcmp("read", flag)) |
| *perm |= BT_ATT_PERM_READ; |
| else if (!strcmp("write", flag)) |
| *perm |= BT_ATT_PERM_WRITE; |
| else if (!strcmp("encrypt-read", flag)) |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_ENCRYPT; |
| else if (!strcmp("encrypt-write", flag)) |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_ENCRYPT; |
| else if (!strcmp("encrypt-authenticated-read", flag)) |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_AUTHEN; |
| else if (!strcmp("encrypt-authenticated-write", flag)) |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_AUTHEN; |
| else if (!strcmp("secure-read", flag)) |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_SECURE; |
| else if (!strcmp("secure-write", flag)) |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_SECURE; |
| else if (!strcmp("authorize", flag)) |
| *req_prep_authorization = true; |
| else { |
| error("Invalid descriptor flag: %s", flag); |
| return false; |
| } |
| } while (dbus_message_iter_next(array)); |
| |
| return true; |
| } |
| |
| static bool parse_flags(GDBusProxy *proxy, uint8_t *props, uint8_t *ext_props, |
| uint32_t *perm, uint32_t *ccc_perm, |
| bool *req_prep_authorization) |
| { |
| DBusMessageIter iter, array; |
| const char *iface; |
| |
| if (!g_dbus_proxy_get_property(proxy, "Flags", &iter)) |
| return false; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) |
| return false; |
| |
| dbus_message_iter_recurse(&iter, &array); |
| |
| iface = g_dbus_proxy_get_interface(proxy); |
| if (!strcmp(iface, GATT_DESC_IFACE)) |
| return parse_desc_flags(&array, perm, req_prep_authorization); |
| |
| return parse_chrc_flags(&array, props, ext_props, perm, ccc_perm, |
| req_prep_authorization); |
| } |
| |
| static struct external_chrc *chrc_create(struct gatt_app *app, |
| GDBusProxy *proxy, |
| const char *path) |
| { |
| struct external_service *service; |
| struct external_chrc *chrc; |
| const char *service_path; |
| |
| if (!parse_path(proxy, "Service", &service_path)) { |
| error("Failed to obtain service path for characteristic"); |
| return NULL; |
| } |
| |
| service = queue_find(app->services, match_service_by_path, |
| service_path); |
| if (!service) { |
| error("Unable to find service for characteristic: %s", path); |
| return NULL; |
| } |
| |
| chrc = new0(struct external_chrc, 1); |
| chrc->pending_reads = queue_new(); |
| chrc->pending_writes = queue_new(); |
| |
| chrc->path = g_strdup(path); |
| if (!chrc->path) |
| goto fail; |
| |
| chrc->service = service; |
| chrc->proxy = g_dbus_proxy_ref(proxy); |
| |
| /* |
| * Add 2 for the characteristic declaration and the value |
| * attribute. |
| */ |
| if (!incr_attr_count(chrc->service, 2)) { |
| error("Failed to increment attribute count"); |
| goto fail; |
| } |
| |
| /* |
| * Parse characteristic flags (i.e. properties) here since they |
| * are used to determine if any special descriptors should be |
| * created. |
| */ |
| if (!parse_flags(proxy, &chrc->props, &chrc->ext_props, &chrc->perm, |
| &chrc->ccc_perm, &chrc->req_prep_authorization)) { |
| error("Failed to parse characteristic properties"); |
| goto fail; |
| } |
| |
| if ((chrc->props & BT_GATT_CHRC_PROP_NOTIFY || |
| chrc->props & BT_GATT_CHRC_PROP_INDICATE) && |
| !incr_attr_count(chrc->service, 1)) { |
| error("Failed to increment attribute count for CCC"); |
| goto fail; |
| } |
| |
| if (chrc->ext_props && !incr_attr_count(chrc->service, 1)) { |
| error("Failed to increment attribute count for CEP"); |
| goto fail; |
| } |
| |
| queue_push_tail(chrc->service->chrcs, chrc); |
| |
| return chrc; |
| |
| fail: |
| chrc_free(chrc); |
| return NULL; |
| } |
| |
| static bool match_chrc(const void *a, const void *b) |
| { |
| const struct external_chrc *chrc = a; |
| const char *path = b; |
| |
| return strcmp(chrc->path, path) == 0; |
| } |
| |
| static bool match_service_by_chrc(const void *a, const void *b) |
| { |
| const struct external_service *service = a; |
| const char *path = b; |
| |
| return queue_find(service->chrcs, match_chrc, path); |
| } |
| |
| static struct external_desc *desc_create(struct gatt_app *app, |
| GDBusProxy *proxy) |
| { |
| struct external_service *service; |
| struct external_desc *desc; |
| const char *chrc_path; |
| |
| if (!parse_path(proxy, "Characteristic", &chrc_path)) { |
| error("Failed to obtain characteristic path for descriptor"); |
| return NULL; |
| } |
| |
| service = queue_find(app->services, match_service_by_chrc, chrc_path); |
| if (!service) { |
| error("Unable to find service for characteristic: %s", |
| chrc_path); |
| return NULL; |
| } |
| |
| desc = new0(struct external_desc, 1); |
| desc->pending_reads = queue_new(); |
| desc->pending_writes = queue_new(); |
| |
| desc->chrc_path = g_strdup(chrc_path); |
| if (!desc->chrc_path) |
| goto fail; |
| |
| desc->service = service; |
| desc->proxy = g_dbus_proxy_ref(proxy); |
| |
| /* Add 1 for the descriptor attribute */ |
| if (!incr_attr_count(desc->service, 1)) { |
| error("Failed to increment attribute count"); |
| goto fail; |
| } |
| |
| /* |
| * Parse descriptors flags here since they are used to |
| * determine the permission the descriptor should have |
| */ |
| if (!parse_flags(proxy, NULL, NULL, &desc->perm, NULL, |
| &desc->req_prep_authorization)) { |
| error("Failed to parse characteristic properties"); |
| goto fail; |
| } |
| |
| queue_push_tail(desc->service->descs, desc); |
| |
| return desc; |
| |
| fail: |
| desc_free(desc); |
| return NULL; |
| } |
| |
| static bool check_service_path(GDBusProxy *proxy, |
| struct external_service *service) |
| { |
| const char *service_path; |
| |
| if (!parse_path(proxy, "Service", &service_path)) |
| return false; |
| |
| return g_strcmp0(service_path, service->path) == 0; |
| } |
| |
| static struct external_service *create_service(struct gatt_app *app, |
| GDBusProxy *proxy, |
| const char *path) |
| { |
| struct external_service *service; |
| |
| if (!path || !g_str_has_prefix(path, "/")) |
| return NULL; |
| |
| service = queue_find(app->services, match_service_by_path, path); |
| if (service) { |
| error("Duplicated service: %s", path); |
| return NULL; |
| } |
| |
| service = new0(struct external_service, 1); |
| |
| service->app = app; |
| |
| service->path = g_strdup(path); |
| if (!service->path) |
| goto fail; |
| |
| service->proxy = g_dbus_proxy_ref(proxy); |
| service->chrcs = queue_new(); |
| service->descs = queue_new(); |
| service->includes = queue_new(); |
| |
| /* Add 1 for the service declaration */ |
| if (!incr_attr_count(service, 1)) { |
| error("Failed to increment attribute count"); |
| goto fail; |
| } |
| |
| queue_push_tail(app->services, service); |
| |
| return service; |
| |
| fail: |
| service_free(service); |
| return NULL; |
| } |
| |
| static void proxy_added_cb(GDBusProxy *proxy, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| const char *iface, *path; |
| |
| if (app->failed) |
| return; |
| |
| queue_push_tail(app->proxies, proxy); |
| |
| iface = g_dbus_proxy_get_interface(proxy); |
| path = g_dbus_proxy_get_path(proxy); |
| |
| DBG("Object received: %s, iface: %s", path, iface); |
| } |
| |
| static void proxy_removed_cb(GDBusProxy *proxy, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| struct external_service *service; |
| const char *path; |
| |
| path = g_dbus_proxy_get_path(proxy); |
| |
| service = queue_remove_if(app->services, match_service_by_path, |
| (void *) path); |
| if (!service) |
| return; |
| |
| DBG("Proxy removed - removing service: %s", service->path); |
| |
| service_free(service); |
| } |
| |
| static bool parse_uuid(GDBusProxy *proxy, bt_uuid_t *uuid) |
| { |
| DBusMessageIter iter; |
| bt_uuid_t tmp; |
| const char *uuidstr; |
| |
| if (!g_dbus_proxy_get_property(proxy, "UUID", &iter)) |
| return false; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) |
| return false; |
| |
| dbus_message_iter_get_basic(&iter, &uuidstr); |
| |
| if (bt_string_to_uuid(uuid, uuidstr) < 0) |
| return false; |
| |
| /* GAP & GATT services are created and managed by BlueZ */ |
| bt_uuid16_create(&tmp, UUID_GAP); |
| if (!bt_uuid_cmp(&tmp, uuid)) { |
| error("GAP service must be handled by BlueZ"); |
| return false; |
| } |
| |
| bt_uuid16_create(&tmp, UUID_GATT); |
| if (!bt_uuid_cmp(&tmp, uuid)) { |
| error("GATT service must be handled by BlueZ"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool parse_includes(GDBusProxy *proxy, struct external_service *service) |
| { |
| DBusMessageIter iter; |
| DBusMessageIter array; |
| char *obj; |
| char *includes; |
| int type; |
| |
| /* Includes property is optional */ |
| if (!g_dbus_proxy_get_property(proxy, "Includes", &iter)) |
| return true; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) |
| return false; |
| |
| dbus_message_iter_recurse(&iter, &array); |
| |
| while ((type = dbus_message_iter_get_arg_type(&array)) |
| != DBUS_TYPE_INVALID) { |
| if (type != DBUS_TYPE_OBJECT_PATH) |
| return false; |
| |
| dbus_message_iter_get_basic(&array, &obj); |
| |
| includes = g_strdup(obj); |
| if (!includes) |
| return false; |
| |
| if (!queue_push_tail(service->includes, includes)) { |
| error("Failed to add Includes path in queue\n"); |
| return false; |
| } |
| |
| incr_attr_count(service, 1); |
| |
| dbus_message_iter_next(&array); |
| } |
| |
| return true; |
| } |
| |
| static bool parse_primary(GDBusProxy *proxy, bool *primary) |
| { |
| DBusMessageIter iter; |
| dbus_bool_t val; |
| |
| if (!g_dbus_proxy_get_property(proxy, "Primary", &iter)) |
| return false; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_BOOLEAN) |
| return false; |
| |
| dbus_message_iter_get_basic(&iter, &val); |
| |
| *primary = val; |
| |
| return true; |
| } |
| |
| static bool parse_handle(GDBusProxy *proxy, uint16_t *handle) |
| { |
| DBusMessageIter iter; |
| |
| *handle = 0; |
| |
| /* Handle property is optional */ |
| if (!g_dbus_proxy_get_property(proxy, "Handle", &iter)) |
| return true; |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT16) |
| return false; |
| |
| dbus_message_iter_get_basic(&iter, handle); |
| |
| return true; |
| } |
| |
| static uint8_t dbus_error_to_att_ecode(const char *name, const char *msg, |
| uint8_t perm_err) |
| { |
| if (strcmp(name, ERROR_INTERFACE ".Failed") == 0) { |
| char *endptr = NULL; |
| uint32_t ecode; |
| |
| ecode = strtol(msg, &endptr, 0); |
| |
| /* If message doesn't set an error code just use 0x80 */ |
| if (!endptr || *endptr != '\0') |
| return 0x80; |
| |
| if (ecode < 0x80 || ecode > 0x9f) { |
| error("Invalid error code: %s", msg); |
| return BT_ATT_ERROR_UNLIKELY; |
| } |
| |
| return ecode; |
| } |
| |
| if (strcmp(name, ERROR_INTERFACE ".NotSupported") == 0) |
| return BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| |
| if (strcmp(name, ERROR_INTERFACE ".NotAuthorized") == 0) |
| return BT_ATT_ERROR_AUTHORIZATION; |
| |
| if (strcmp(name, ERROR_INTERFACE ".InvalidValueLength") == 0) |
| return BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN; |
| |
| if (strcmp(name, ERROR_INTERFACE ".InvalidOffset") == 0) |
| return BT_ATT_ERROR_INVALID_OFFSET; |
| |
| if (strcmp(name, ERROR_INTERFACE ".InProgress") == 0) |
| return BT_ERROR_ALREADY_IN_PROGRESS; |
| |
| if (!strcmp(name, ERROR_INTERFACE ".ImproperlyConfigured")) |
| return BT_ERROR_CCC_IMPROPERLY_CONFIGURED; |
| |
| if (strcmp(name, ERROR_INTERFACE ".NotPermitted") == 0) |
| return perm_err; |
| |
| return BT_ATT_ERROR_UNLIKELY; |
| } |
| |
| static void read_reply_cb(DBusMessage *message, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| DBusError err; |
| DBusMessageIter iter, array; |
| uint8_t ecode = 0; |
| uint8_t *value = NULL; |
| int len = 0; |
| |
| if (!op->owner_queue) { |
| DBG("Pending read was canceled when object got removed"); |
| return; |
| } |
| |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| DBG("Failed to read value: %s: %s", err.name, err.message); |
| ecode = dbus_error_to_att_ecode(err.name, err.message, |
| BT_ATT_ERROR_READ_NOT_PERMITTED); |
| dbus_error_free(&err); |
| goto done; |
| } |
| |
| dbus_message_iter_init(message, &iter); |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { |
| /* |
| * Return not supported for this, as the external app basically |
| * doesn't properly support reading from this characteristic. |
| */ |
| ecode = BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| error("Invalid return value received for \"ReadValue\""); |
| goto done; |
| } |
| |
| dbus_message_iter_recurse(&iter, &array); |
| dbus_message_iter_get_fixed_array(&array, &value, &len); |
| |
| if (len < 0) { |
| ecode = BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| value = NULL; |
| len = 0; |
| goto done; |
| } |
| |
| /* Truncate the value if it's too large */ |
| len = MIN(BT_ATT_MAX_VALUE_LEN, len); |
| value = len ? value : NULL; |
| |
| done: |
| gatt_db_attribute_read_result(op->attrib, op->id, ecode, value, len); |
| } |
| |
| |
| static struct pending_op *pending_read_new(struct bt_att *att, |
| struct queue *owner_queue, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset) |
| { |
| struct pending_op *op; |
| |
| op = new0(struct pending_op, 1); |
| |
| op->owner_queue = owner_queue; |
| op->att = bt_att_ref(att); |
| op->attrib = attrib; |
| op->id = id; |
| op->offset = offset; |
| op->link_type = bt_att_get_link_type(att); |
| queue_push_tail(owner_queue, op); |
| |
| op->disconn_id = bt_att_register_disconnect(att, pending_disconnect_cb, |
| op, NULL); |
| |
| return op; |
| } |
| |
| static void append_options(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| struct btd_device *device = att_get_device(op->att); |
| const char *path = device_get_path(device); |
| struct bt_gatt_server *server; |
| const char *link; |
| uint16_t mtu; |
| |
| switch (op->link_type) { |
| case BT_ATT_BREDR: |
| link = "BR/EDR"; |
| break; |
| case BT_ATT_LE: |
| link = "LE"; |
| break; |
| default: |
| link = NULL; |
| break; |
| } |
| |
| dict_append_entry(iter, "device", DBUS_TYPE_OBJECT_PATH, &path); |
| if (op->offset) |
| dict_append_entry(iter, "offset", DBUS_TYPE_UINT16, |
| &op->offset); |
| if (link) |
| dict_append_entry(iter, "link", DBUS_TYPE_STRING, &link); |
| if (op->prep_authorize) |
| dict_append_entry(iter, "prepare-authorize", DBUS_TYPE_BOOLEAN, |
| &op->prep_authorize); |
| |
| server = btd_device_get_gatt_server(device); |
| mtu = bt_gatt_server_get_mtu(server); |
| |
| dict_append_entry(iter, "mtu", DBUS_TYPE_UINT16, &mtu); |
| } |
| |
| static void read_setup_cb(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| append_options(&dict, op); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static struct pending_op *send_read(struct bt_att *att, |
| struct gatt_db_attribute *attrib, |
| GDBusProxy *proxy, |
| struct queue *owner_queue, |
| unsigned int id, |
| uint16_t offset) |
| { |
| struct pending_op *op; |
| |
| op = pending_read_new(att, owner_queue, attrib, id, offset); |
| |
| if (g_dbus_proxy_method_call(proxy, "ReadValue", read_setup_cb, |
| read_reply_cb, op, pending_op_free) == TRUE) |
| return op; |
| |
| pending_op_free(op); |
| |
| return NULL; |
| } |
| |
| static void write_setup_cb(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| DBusMessageIter array, dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "y", &array); |
| dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, |
| &op->data.iov_base, op->data.iov_len); |
| dbus_message_iter_close_container(iter, &array); |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| append_options(&dict, op); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| |
| if (!op->owner_queue) { |
| gatt_db_attribute_write_result(op->attrib, op->id, 0); |
| pending_op_free(op); |
| } |
| } |
| |
| static void write_reply_cb(DBusMessage *message, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| struct external_chrc *chrc; |
| struct external_desc *desc; |
| DBusError err; |
| DBusMessageIter iter; |
| uint8_t ecode = 0; |
| |
| if (!op->owner_queue) { |
| DBG("Pending write was canceled when object got removed"); |
| return; |
| } |
| |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| DBG("Failed to write value: %s: %s", err.name, err.message); |
| ecode = dbus_error_to_att_ecode(err.name, err.message, |
| BT_ATT_ERROR_WRITE_NOT_PERMITTED); |
| dbus_error_free(&err); |
| goto done; |
| } |
| |
| if (op->prep_authorize) { |
| if (op->is_characteristic) { |
| chrc = gatt_db_attribute_get_user_data(op->attrib); |
| chrc->prep_authorized = true; |
| } else { |
| desc = gatt_db_attribute_get_user_data(op->attrib); |
| desc->prep_authorized = true; |
| } |
| } |
| |
| dbus_message_iter_init(message, &iter); |
| if (dbus_message_iter_has_next(&iter)) { |
| /* |
| * Return not supported for this, as the external app basically |
| * doesn't properly support the "WriteValue" API. |
| */ |
| ecode = BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| error("Invalid return value received for \"WriteValue\""); |
| } |
| |
| done: |
| gatt_db_attribute_write_result(op->attrib, op->id, ecode); |
| } |
| |
| static struct pending_op *pending_write_new(struct bt_att *att, |
| struct queue *owner_queue, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, |
| const uint8_t *value, size_t len, |
| uint16_t offset, |
| bool is_characteristic, |
| bool prep_authorize) |
| { |
| struct pending_op *op; |
| |
| op = new0(struct pending_op, 1); |
| |
| op->data.iov_base = (uint8_t *) value; |
| op->data.iov_len = len; |
| |
| op->att = bt_att_ref(att); |
| op->owner_queue = owner_queue; |
| op->attrib = attrib; |
| op->id = id; |
| op->offset = offset; |
| op->link_type = bt_att_get_link_type(att); |
| op->is_characteristic = is_characteristic; |
| op->prep_authorize = prep_authorize; |
| queue_push_tail(owner_queue, op); |
| |
| op->disconn_id = bt_att_register_disconnect(att, |
| pending_disconnect_cb, |
| op, NULL); |
| |
| return op; |
| } |
| |
| static struct pending_op *send_write(struct bt_att *att, |
| struct gatt_db_attribute *attrib, |
| GDBusProxy *proxy, |
| struct queue *owner_queue, |
| unsigned int id, |
| const uint8_t *value, size_t len, |
| uint16_t offset, |
| bool is_characteristic, |
| bool prep_authorize) |
| { |
| struct pending_op *op; |
| |
| op = pending_write_new(att, owner_queue, attrib, id, value, len, |
| offset, is_characteristic, |
| prep_authorize); |
| |
| if (g_dbus_proxy_method_call(proxy, "WriteValue", write_setup_cb, |
| owner_queue ? write_reply_cb : NULL, |
| op, pending_op_free) == TRUE) |
| return op; |
| |
| pending_op_free(op); |
| |
| return NULL; |
| } |
| |
| static void flush_pending_write(void *data, void *user_data) |
| { |
| GDBusProxy *proxy = user_data; |
| struct pending_op *op = data; |
| |
| if (g_dbus_proxy_method_call(proxy, "WriteValue", write_setup_cb, |
| write_reply_cb, |
| op, pending_op_free) == TRUE) |
| return; |
| |
| pending_op_free(op); |
| } |
| |
| static void flush_pending_writes(GDBusProxy *proxy, |
| struct queue *owner_queue) |
| { |
| queue_foreach(owner_queue, flush_pending_write, proxy); |
| queue_remove_all(owner_queue, NULL, NULL, NULL); |
| } |
| |
| static bool match_client_io(const void *data, const void *user_data) |
| { |
| const struct client_io *client = data; |
| const struct io *io = user_data; |
| |
| return client->io == io; |
| } |
| |
| static bool sock_hup(struct io *io, void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| struct client_io *client; |
| |
| DBG("%p closed\n", io); |
| |
| client = queue_remove_if(chrc->write_ios, match_client_io, io); |
| if (!client) { |
| client = queue_remove_if(chrc->notify_ios, match_client_io, io); |
| if (!client) |
| return false; |
| } |
| |
| client_io_free(client); |
| |
| return false; |
| } |
| |
| static bool sock_io_write(struct io *io, void *user_data) |
| { |
| uint8_t buf[] = { 1 }; |
| struct iovec iov = { buf, sizeof(buf) }; |
| |
| /* Send a 1 to the server as confirmation */ |
| io_send(io, &iov, 1); |
| |
| return false; |
| } |
| |
| static void sock_io_conf(void *user_data) |
| { |
| struct io *io = user_data; |
| |
| io_set_write_handler(io, sock_io_write, NULL, NULL); |
| } |
| |
| static bool sock_io_read(struct io *io, void *user_data) |
| { |
| struct client_io *client = user_data; |
| struct external_chrc *chrc = client->chrc; |
| uint8_t buf[512]; |
| int fd = io_get_fd(io); |
| ssize_t bytes_read; |
| struct notify notify; |
| struct device_state *state; |
| |
| if (fd < 0) { |
| error("io_get_fd() returned %d\n", fd); |
| return false; |
| } |
| |
| bytes_read = read(fd, buf, sizeof(buf)); |
| if (bytes_read <= 0) |
| return false; |
| |
| memset(¬ify, 0, sizeof(notify)); |
| |
| notify.database = client->chrc->service->app->database; |
| notify.handle = gatt_db_attribute_get_handle(chrc->attrib); |
| notify.ccc_handle = gatt_db_attribute_get_handle(chrc->ccc); |
| notify.value = (void *) buf; |
| notify.len = bytes_read; |
| notify.conf = sock_io_conf; |
| notify.user_data = io; |
| |
| |
| state = find_device_state_by_att(notify.database, client->att); |
| if (!state) |
| return false; |
| |
| send_notification_to_device(state, ¬ify); |
| |
| return true; |
| } |
| |
| static struct io *sock_io_new(int fd, void *user_data) |
| { |
| struct io *io; |
| |
| io = io_new(fd); |
| |
| io_set_close_on_destroy(io, true); |
| |
| io_set_disconnect_handler(io, sock_hup, user_data, NULL); |
| |
| return io; |
| } |
| |
| static int sock_io_send(struct io *io, const void *data, size_t len) |
| { |
| struct msghdr msg; |
| struct iovec iov; |
| int fd; |
| |
| iov.iov_base = (void *) data; |
| iov.iov_len = len; |
| |
| memset(&msg, 0, sizeof(msg)); |
| msg.msg_iov = &iov; |
| msg.msg_iovlen = 1; |
| |
| fd = io_get_fd(io); |
| if (fd < 0) { |
| error("io_get_fd() returned %d\n", fd); |
| return fd; |
| } |
| |
| return sendmsg(fd, &msg, MSG_NOSIGNAL); |
| } |
| |
| static void att_disconnect_cb(int err, void *user_data) |
| { |
| struct client_io *client = user_data; |
| |
| /* If ATT is disconnected shutdown correspondent client IO so sock_hup |
| * is triggered and the server socket is closed. |
| */ |
| io_shutdown(client->io); |
| } |
| |
| static struct client_io * |
| client_io_new(struct external_chrc *chrc, int fd, struct bt_att *att) |
| { |
| struct client_io *client; |
| |
| client = new0(struct client_io, 1); |
| client->att = bt_att_ref(att); |
| client->chrc = chrc; |
| client->disconn_id = bt_att_register_disconnect(att, att_disconnect_cb, |
| client, NULL); |
| client->io = sock_io_new(fd, chrc); |
| |
| return client; |
| } |
| |
| static bool match_client_att(const void *data, const void *user_data) |
| { |
| const struct client_io *client = data; |
| const struct bt_att *att = user_data; |
| |
| /* Always match if ATT instance is not set since that is used by |
| * clear_cc_state to clear all instances. |
| */ |
| if (!att) |
| return true; |
| |
| return client->att == att; |
| } |
| |
| static struct client_io * |
| client_write_io_get(struct external_chrc *chrc, int fd, struct bt_att *att) |
| { |
| struct client_io *client; |
| |
| client = queue_find(chrc->write_ios, match_client_att, att); |
| if (client) |
| return client; |
| |
| client = client_io_new(chrc, fd, att); |
| |
| if (!chrc->write_ios) |
| chrc->write_ios = queue_new(); |
| |
| queue_push_tail(chrc->write_ios, client); |
| |
| return client; |
| } |
| |
| static void acquire_write_reply(DBusMessage *message, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| struct external_chrc *chrc; |
| struct client_io *client; |
| DBusError err; |
| int fd; |
| uint16_t mtu; |
| |
| if (!op->owner_queue) { |
| DBG("Pending write was canceled when object got removed"); |
| return; |
| } |
| |
| chrc = gatt_db_attribute_get_user_data(op->attrib); |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| uint8_t ecode; |
| |
| error("Failed to acquire write: %s\n", err.name); |
| |
| ecode = dbus_error_to_att_ecode(err.name, err.message, |
| BT_ATT_ERROR_WRITE_NOT_PERMITTED); |
| dbus_error_free(&err); |
| |
| if (ecode != BT_ATT_ERROR_UNLIKELY) { |
| gatt_db_attribute_write_result(op->attrib, op->id, |
| ecode); |
| pending_op_free(op); |
| return; |
| } |
| |
| goto retry; |
| } |
| |
| if ((dbus_message_get_args(message, NULL, DBUS_TYPE_UNIX_FD, &fd, |
| DBUS_TYPE_UINT16, &mtu, |
| DBUS_TYPE_INVALID) == false)) { |
| error("Invalid AcquireWrite response\n"); |
| goto retry; |
| } |
| |
| DBG("AcquireWrite success: fd %d MTU %u\n", fd, mtu); |
| |
| client = client_write_io_get(chrc, fd, op->att); |
| if (!client) |
| goto retry; |
| |
| while ((op = queue_peek_head(chrc->pending_writes)) != NULL) { |
| if (sock_io_send(client->io, op->data.iov_base, |
| op->data.iov_len) < 0) |
| goto retry; |
| |
| gatt_db_attribute_write_result(op->attrib, op->id, 0); |
| pending_op_free(op); |
| } |
| |
| return; |
| |
| retry: |
| flush_pending_writes(chrc->proxy, chrc->pending_writes); |
| } |
| |
| static void acquire_write_setup(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| append_options(&dict, op); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static struct pending_op *acquire_write(struct external_chrc *chrc, |
| struct bt_att *att, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, |
| const uint8_t *value, size_t len) |
| { |
| struct pending_op *op; |
| bool acquiring = !queue_isempty(chrc->pending_writes); |
| |
| op = pending_write_new(att, chrc->pending_writes, attrib, id, value, |
| len, 0, false, false); |
| |
| if (acquiring) |
| return op; |
| |
| if (g_dbus_proxy_method_call(chrc->proxy, "AcquireWrite", |
| acquire_write_setup, |
| acquire_write_reply, |
| op, NULL)) |
| return op; |
| |
| pending_op_free(op); |
| |
| return NULL; |
| } |
| |
| static struct client_io * |
| client_notify_io_get(struct external_chrc *chrc, int fd, struct bt_att *att) |
| { |
| struct client_io *client; |
| |
| client = queue_find(chrc->notify_ios, match_client_att, att); |
| if (client) |
| return client; |
| |
| client = client_io_new(chrc, fd, att); |
| |
| io_set_read_handler(client->io, sock_io_read, client, NULL); |
| |
| if (!chrc->notify_ios) |
| chrc->notify_ios = queue_new(); |
| |
| queue_push_tail(chrc->notify_ios, client); |
| |
| return client; |
| } |
| |
| static void acquire_notify_reply(DBusMessage *message, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| struct external_chrc *chrc = (void *) op->data.iov_base; |
| struct client_io *client; |
| DBusError err; |
| int fd; |
| uint16_t mtu; |
| |
| if (!op->owner_queue) { |
| DBG("Pending notify was canceled when object got removed"); |
| return; |
| } |
| |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| error("Failed to acquire notify: %s\n", err.name); |
| |
| if (dbus_error_has_name(&err, DBUS_ERROR_NO_REPLY) || |
| dbus_error_has_name(&err, ERROR_FAILED)) { |
| dbus_error_free(&err); |
| return; |
| } |
| |
| dbus_error_free(&err); |
| goto retry; |
| } |
| |
| if ((dbus_message_get_args(message, NULL, DBUS_TYPE_UNIX_FD, &fd, |
| DBUS_TYPE_UINT16, &mtu, |
| DBUS_TYPE_INVALID) == false)) { |
| error("Invalid AcquirNotify response\n"); |
| goto retry; |
| } |
| |
| DBG("AcquireNotify success: fd %d MTU %u\n", fd, mtu); |
| |
| client = client_notify_io_get(chrc, fd, op->att); |
| if (!client) |
| goto retry; |
| |
| __sync_fetch_and_add(&chrc->ntfy_cnt, 1); |
| |
| return; |
| |
| retry: |
| g_dbus_proxy_method_call(chrc->proxy, "StartNotify", NULL, NULL, |
| NULL, NULL); |
| |
| __sync_fetch_and_add(&chrc->ntfy_cnt, 1); |
| } |
| |
| static void acquire_notify_setup(DBusMessageIter *iter, void *user_data) |
| { |
| DBusMessageIter dict; |
| struct pending_op *op = user_data; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| append_options(&dict, op); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static uint8_t ccc_write_cb(struct pending_op *op, void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| struct client_io *client; |
| DBusMessageIter iter; |
| uint16_t value; |
| |
| value = op ? PTR_TO_UINT(op->data.iov_base) : 0; |
| |
| DBG("External CCC write received with value: 0x%04x", value); |
| |
| /* Notifications/indications disabled */ |
| if (!value) { |
| if (!chrc->ntfy_cnt) |
| goto done; |
| |
| client = queue_remove_if(chrc->notify_ios, match_client_att, |
| op ? op->att : NULL); |
| if (client) { |
| client_io_free(client); |
| __sync_sub_and_fetch(&chrc->ntfy_cnt, 1); |
| goto done; |
| } |
| |
| if (__sync_sub_and_fetch(&chrc->ntfy_cnt, 1)) |
| goto done; |
| |
| /* |
| * Send request to stop notifying. This is best-effort |
| * operation, so simply ignore the return the value. |
| */ |
| g_dbus_proxy_method_call(chrc->proxy, "StopNotify", NULL, |
| NULL, NULL, NULL); |
| goto done; |
| } |
| |
| if (chrc->ntfy_cnt == UINT_MAX) |
| /* Maximum number of per-device CCC descriptors configured */ |
| return BT_ATT_ERROR_INSUFFICIENT_RESOURCES; |
| |
| /* Don't support undefined CCC values yet */ |
| if (value > 2 || |
| (value == 1 && !(chrc->props & BT_GATT_CHRC_PROP_NOTIFY)) || |
| (value == 2 && !(chrc->props & BT_GATT_CHRC_PROP_INDICATE))) |
| return BT_ERROR_CCC_IMPROPERLY_CONFIGURED; |
| |
| client = queue_find(chrc->notify_ios, match_client_att, op->att); |
| if (client) { |
| __sync_fetch_and_add(&chrc->ntfy_cnt, 1); |
| goto done; |
| } |
| |
| /* Make use of AcquireNotify if supported */ |
| if (g_dbus_proxy_get_property(chrc->proxy, "NotifyAcquired", &iter)) { |
| op->data.iov_base = (void *) chrc; |
| op->data.iov_len = sizeof(chrc); |
| op->owner_queue = chrc->pending_writes; |
| if (g_dbus_proxy_method_call(chrc->proxy, "AcquireNotify", |
| acquire_notify_setup, |
| acquire_notify_reply, |
| op, pending_op_free)) |
| return 0; |
| } |
| |
| /* |
| * Always call StartNotify for an incoming enable and ignore the return |
| * value for now. |
| */ |
| if (g_dbus_proxy_method_call(chrc->proxy, "StartNotify", NULL, NULL, |
| NULL, NULL) == FALSE) |
| return BT_ATT_ERROR_UNLIKELY; |
| |
| __sync_fetch_and_add(&chrc->ntfy_cnt, 1); |
| |
| done: |
| if (op) |
| pending_op_free(op); |
| |
| return 0; |
| } |
| |
| static void property_changed_cb(GDBusProxy *proxy, const char *name, |
| DBusMessageIter *iter, void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| DBusMessageIter array; |
| uint8_t *value = NULL; |
| int len = 0; |
| |
| if (strcmp(name, "Value")) |
| return; |
| |
| if (iter) { |
| if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) { |
| DBG("Malformed \"Value\" property received"); |
| return; |
| } |
| |
| dbus_message_iter_recurse(iter, &array); |
| dbus_message_iter_get_fixed_array(&array, &value, &len); |
| |
| if (len < 0) { |
| DBG("Malformed \"Value\" property received"); |
| return; |
| } |
| } |
| |
| /* Truncate the value if it's too large */ |
| len = MIN(BT_ATT_MAX_VALUE_LEN, len); |
| value = len ? value : NULL; |
| |
| send_notification_to_devices(chrc->service->app->database, |
| gatt_db_attribute_get_handle(chrc->attrib), |
| value, len, |
| gatt_db_attribute_get_handle(chrc->ccc), |
| conf_cb, |
| proxy); |
| } |
| |
| static bool database_add_ccc(struct external_service *service, |
| struct external_chrc *chrc) |
| { |
| if (!(chrc->props & BT_GATT_CHRC_PROP_NOTIFY) && |
| !(chrc->props & BT_GATT_CHRC_PROP_INDICATE)) |
| return true; |
| |
| /* Always set read/write permissions */ |
| chrc->ccc_perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_READ; |
| |
| chrc->ccc = service_add_ccc(service->attrib, service->app->database, |
| ccc_write_cb, chrc, chrc->ccc_perm, NULL); |
| if (!chrc->ccc) { |
| error("Failed to create CCC entry for characteristic"); |
| return false; |
| } |
| |
| if (g_dbus_proxy_set_property_watch(chrc->proxy, property_changed_cb, |
| chrc) == FALSE) { |
| error("Failed to set up property watch for characteristic"); |
| return false; |
| } |
| |
| DBG("Created CCC entry for characteristic"); |
| |
| return true; |
| } |
| |
| static void cep_write_cb(struct gatt_db_attribute *attrib, int err, |
| void *user_data) |
| { |
| if (err) |
| DBG("Failed to store CEP value in the database"); |
| else |
| DBG("Stored CEP value in the database"); |
| } |
| |
| static bool database_add_cep(struct external_service *service, |
| struct external_chrc *chrc) |
| { |
| struct gatt_db_attribute *cep; |
| bt_uuid_t uuid; |
| uint8_t value[2]; |
| |
| if (!chrc->ext_props) |
| return true; |
| |
| bt_uuid16_create(&uuid, GATT_CHARAC_EXT_PROPER_UUID); |
| cep = gatt_db_service_add_descriptor(service->attrib, &uuid, |
| BT_ATT_PERM_READ, |
| NULL, NULL, NULL); |
| if (!cep) { |
| error("Failed to create CEP entry for characteristic"); |
| return false; |
| } |
| |
| memset(value, 0, sizeof(value)); |
| value[0] = chrc->ext_props; |
| |
| if (!gatt_db_attribute_write(cep, 0, value, sizeof(value), 0, NULL, |
| cep_write_cb, NULL)) { |
| DBG("Failed to store CEP value in the database"); |
| return false; |
| } |
| |
| DBG("Created CEP entry for characteristic"); |
| |
| return true; |
| } |
| |
| static void desc_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct external_desc *desc = user_data; |
| struct btd_device *device; |
| |
| if (desc->attrib != attrib) { |
| error("Read callback called with incorrect attribute"); |
| goto fail; |
| } |
| |
| device = att_get_device(att); |
| if (!device) { |
| error("Unable to find device object"); |
| goto fail; |
| } |
| |
| if (send_read(att, attrib, desc->proxy, desc->pending_reads, id, |
| offset)) |
| return; |
| |
| fail: |
| gatt_db_attribute_read_result(attrib, id, BT_ATT_ERROR_UNLIKELY, |
| NULL, 0); |
| } |
| |
| static void desc_write_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| const uint8_t *value, size_t len, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct external_desc *desc = user_data; |
| struct btd_device *device; |
| |
| if (desc->attrib != attrib) { |
| error("Read callback called with incorrect attribute"); |
| goto fail; |
| } |
| |
| device = att_get_device(att); |
| if (!device) { |
| error("Unable to find device object"); |
| goto fail; |
| } |
| |
| if (opcode == BT_ATT_OP_PREP_WRITE_REQ) { |
| if (!btd_device_is_trusted(device) && !desc->prep_authorized && |
| desc->req_prep_authorization) |
| send_write(att, attrib, desc->proxy, |
| desc->pending_writes, id, value, len, |
| offset, false, true); |
| else |
| gatt_db_attribute_write_result(attrib, id, 0); |
| |
| return; |
| } |
| |
| if (opcode == BT_ATT_OP_EXEC_WRITE_REQ) |
| desc->prep_authorized = false; |
| |
| if (send_write(att, attrib, desc->proxy, desc->pending_writes, id, |
| value, len, offset, false, false)) |
| return; |
| |
| fail: |
| gatt_db_attribute_write_result(attrib, id, BT_ATT_ERROR_UNLIKELY); |
| } |
| |
| static void write_handle(struct GDBusProxy *proxy, uint16_t handle) |
| { |
| DBusMessageIter iter; |
| |
| /* Check if the attribute has the Handle property */ |
| if (!g_dbus_proxy_get_property(proxy, "Handle", &iter)) |
| return; |
| |
| g_dbus_proxy_set_property_basic(proxy, "Handle", DBUS_TYPE_UINT16, |
| &handle, NULL, NULL, NULL); |
| } |
| |
| static bool database_add_desc(struct external_service *service, |
| struct external_desc *desc) |
| { |
| uint16_t handle; |
| bt_uuid_t uuid; |
| char str[MAX_LEN_UUID_STR]; |
| |
| if (!parse_handle(desc->proxy, &handle)) { |
| error("Failed to read \"Handle\" property of descriptor"); |
| return false; |
| } |
| |
| if (!parse_uuid(desc->proxy, &uuid)) { |
| error("Failed to read \"UUID\" property of descriptor"); |
| return false; |
| } |
| |
| desc->attrib = gatt_db_service_insert_descriptor(service->attrib, |
| handle, &uuid, |
| desc->perm, |
| desc_read_cb, |
| desc_write_cb, desc); |
| if (!desc->attrib) { |
| error("Failed to create descriptor entry in database"); |
| return false; |
| } |
| |
| desc->handled = true; |
| |
| if (!handle) { |
| handle = gatt_db_attribute_get_handle(desc->attrib); |
| write_handle(desc->proxy, handle); |
| } |
| |
| bt_uuid_to_string(&uuid, str, sizeof(str)); |
| |
| DBG("handle 0x%04x UUID %s", handle, str); |
| |
| return true; |
| } |
| |
| static void chrc_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| struct btd_device *device; |
| |
| if (chrc->attrib != attrib) { |
| error("Read callback called with incorrect attribute"); |
| goto fail; |
| } |
| |
| device = att_get_device(att); |
| if (!device) { |
| error("Unable to find device object"); |
| goto fail; |
| } |
| |
| if (send_read(att, attrib, chrc->proxy, chrc->pending_reads, id, |
| offset)) |
| return; |
| |
| fail: |
| gatt_db_attribute_read_result(attrib, id, BT_ATT_ERROR_UNLIKELY, |
| NULL, 0); |
| } |
| |
| static void chrc_write_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| const uint8_t *value, size_t len, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| struct client_io *client; |
| struct btd_device *device; |
| struct queue *queue; |
| DBusMessageIter iter; |
| |
| if (chrc->attrib != attrib) { |
| error("Write callback called with incorrect attribute"); |
| goto fail; |
| } |
| |
| device = att_get_device(att); |
| if (!device) { |
| error("Unable to find device object"); |
| goto fail; |
| } |
| |
| if (!(chrc->props & BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP)) |
| queue = chrc->pending_writes; |
| else |
| queue = NULL; |
| |
| if (opcode == BT_ATT_OP_PREP_WRITE_REQ) { |
| if (!btd_device_is_trusted(device) && !chrc->prep_authorized && |
| chrc->req_prep_authorization) |
| send_write(att, attrib, chrc->proxy, queue, |
| id, value, len, offset, |
| true, true); |
| else |
| gatt_db_attribute_write_result(attrib, id, 0); |
| |
| return; |
| } |
| |
| if (opcode == BT_ATT_OP_EXEC_WRITE_REQ) |
| chrc->prep_authorized = false; |
| |
| client = queue_find(chrc->write_ios, match_client_att, att); |
| if (client) { |
| if (sock_io_send(client->io, value, len) < 0) { |
| error("Unable to write: %s", strerror(errno)); |
| goto fail; |
| } |
| |
| gatt_db_attribute_write_result(attrib, id, 0); |
| return; |
| } |
| |
| if (g_dbus_proxy_get_property(chrc->proxy, "WriteAcquired", &iter)) { |
| if (acquire_write(chrc, att, attrib, id, value, len)) |
| return; |
| } |
| |
| if (send_write(att, attrib, chrc->proxy, queue, id, value, len, |
| offset, false, false)) |
| return; |
| |
| fail: |
| gatt_db_attribute_write_result(attrib, id, BT_ATT_ERROR_UNLIKELY); |
| } |
| |
| static void include_services(void *data ,void *userdata) |
| { |
| char *obj = data; |
| struct external_service *service = userdata; |
| struct gatt_db_attribute *attrib; |
| struct external_service *service_inc; |
| |
| DBG("path %s", obj); |
| |
| service_inc = queue_find(service->app->services, match_service_by_path, |
| obj); |
| if (!service_inc) { |
| error("include service not found\n"); |
| return; |
| } |
| |
| attrib = gatt_db_service_add_included(service->attrib, |
| service_inc->attrib); |
| if (!attrib) { |
| error("include service attributes failed\n"); |
| return; |
| } |
| |
| service->attrib = attrib; |
| } |
| |
| static void database_add_includes(struct external_service *service) |
| { |
| queue_foreach(service->includes, include_services, service); |
| } |
| |
| static bool database_add_chrc(struct external_service *service, |
| struct external_chrc *chrc) |
| { |
| uint16_t handle = 0, value_handle; |
| bt_uuid_t uuid; |
| char str[MAX_LEN_UUID_STR]; |
| const struct queue_entry *entry; |
| |
| if (!parse_handle(chrc->proxy, &value_handle)) { |
| error("Failed to read \"Handle\" property of characteristic"); |
| return false; |
| } |
| |
| if (!parse_uuid(chrc->proxy, &uuid)) { |
| error("Failed to read \"UUID\" property of characteristic"); |
| return false; |
| } |
| |
| if (!check_service_path(chrc->proxy, service)) { |
| error("Invalid service path for characteristic"); |
| return false; |
| } |
| |
| if (value_handle) |
| handle = value_handle - 1; |
| |
| chrc->attrib = gatt_db_service_insert_characteristic(service->attrib, |
| handle, value_handle, &uuid, |
| chrc->perm, chrc->props, |
| chrc_read_cb, chrc_write_cb, |
| chrc); |
| if (!chrc->attrib) { |
| error("Failed to create characteristic entry in database"); |
| return false; |
| } |
| |
| if (!database_add_ccc(service, chrc)) |
| return false; |
| |
| if (!database_add_cep(service, chrc)) |
| return false; |
| |
| if (!handle) { |
| handle = gatt_db_attribute_get_handle(chrc->attrib); |
| write_handle(chrc->proxy, handle); |
| } |
| |
| bt_uuid_to_string(&uuid, str, sizeof(str)); |
| |
| DBG("handle 0x%04x UUID %s", handle, str); |
| |
| /* Handle the descriptors that belong to this characteristic. */ |
| for (entry = queue_get_entries(service->descs); entry; |
| entry = entry->next) { |
| struct external_desc *desc = entry->data; |
| |
| if (desc->handled || g_strcmp0(desc->chrc_path, chrc->path)) |
| continue; |
| |
| if (!database_add_desc(service, desc)) { |
| chrc->attrib = NULL; |
| error("Failed to create descriptor entry"); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static bool match_desc_unhandled(const void *a, const void *b) |
| { |
| const struct external_desc *desc = a; |
| |
| return !desc->handled; |
| } |
| |
| static bool database_add_service(struct external_service *service) |
| { |
| bt_uuid_t uuid; |
| bool primary; |
| uint16_t handle; |
| const struct queue_entry *entry; |
| char str[MAX_LEN_UUID_STR]; |
| |
| if (!parse_uuid(service->proxy, &uuid)) { |
| error("Failed to read \"UUID\" property of service"); |
| return false; |
| } |
| |
| if (!parse_primary(service->proxy, &primary)) { |
| error("Failed to read \"Primary\" property of service"); |
| return false; |
| } |
| |
| if (!parse_includes(service->proxy, service)) { |
| error("Failed to read \"Includes\" property of service"); |
| return false; |
| } |
| |
| if (!parse_handle(service->proxy, &handle)) { |
| error("Failed to read \"Handle\" property of service"); |
| return false; |
| } |
| |
| service->attrib = gatt_db_insert_service(service->app->database->db, |
| handle, &uuid, |
| primary, service->attr_cnt); |
| if (!service->attrib) |
| return false; |
| |
| if (!handle) { |
| handle = gatt_db_attribute_get_handle(service->attrib); |
| write_handle(service->proxy, handle); |
| } |
| |
| bt_uuid_to_string(&uuid, str, sizeof(str)); |
| |
| DBG("handle 0x%04x UUID %s", handle, str); |
| |
| database_add_includes(service); |
| |
| entry = queue_get_entries(service->chrcs); |
| while (entry) { |
| struct external_chrc *chrc = entry->data; |
| |
| if (!database_add_chrc(service, chrc)) { |
| error("Failed to add characteristic"); |
| goto fail; |
| } |
| |
| entry = entry->next; |
| } |
| |
| /* If there are any unhandled descriptors, return an error */ |
| if (queue_find(service->descs, match_desc_unhandled, NULL)) { |
| error("Found descriptor with no matching characteristic!"); |
| goto fail; |
| } |
| |
| gatt_db_service_set_active(service->attrib, true); |
| |
| return true; |
| |
| fail: |
| gatt_db_remove_service(service->app->database->db, service->attrib); |
| service->attrib = NULL; |
| |
| return false; |
| } |
| |
| static bool database_add_app(struct gatt_app *app) |
| { |
| const struct queue_entry *entry; |
| |
| entry = queue_get_entries(app->services); |
| while (entry) { |
| if (!database_add_service(entry->data)) { |
| error("Failed to add service"); |
| return false; |
| } |
| |
| entry = entry->next; |
| } |
| |
| return true; |
| } |
| |
| static int profile_device_probe(struct btd_service *service) |
| { |
| struct btd_profile *p = btd_service_get_profile(service); |
| |
| DBG("%s probed", p->name); |
| |
| return 0; |
| } |
| |
| static void profile_device_remove(struct btd_service *service) |
| { |
| struct btd_profile *p = btd_service_get_profile(service); |
| |
| DBG("%s removed", p->name); |
| } |
| |
| static int profile_device_accept(struct btd_service *service) |
| { |
| struct btd_profile *p = btd_service_get_profile(service); |
| |
| DBG("%s accept", p->name); |
| |
| return 0; |
| } |
| |
| static int profile_add(struct external_profile *profile, const char *uuid) |
| { |
| struct btd_profile *p; |
| |
| p = new0(struct btd_profile, 1); |
| |
| /* Assign directly to avoid having extra fields */ |
| p->name = (const void *) g_strdup_printf("%s%s/%s", profile->app->owner, |
| g_dbus_proxy_get_path(profile->proxy), uuid); |
| if (!p->name) { |
| free(p); |
| return -ENOMEM; |
| } |
| |
| p->remote_uuid = (const void *) g_strdup(uuid); |
| if (!p->remote_uuid) { |
| g_free((void *) p->name); |
| free(p); |
| return -ENOMEM; |
| } |
| |
| p->device_probe = profile_device_probe; |
| p->device_remove = profile_device_remove; |
| p->accept = profile_device_accept; |
| p->auto_connect = true; |
| p->external = true; |
| |
| queue_push_tail(profile->profiles, p); |
| |
| DBG("Added \"%s\"", p->name); |
| |
| return 0; |
| } |
| |
| static void add_profile(void *data, void *user_data) |
| { |
| struct btd_adapter *adapter = user_data; |
| |
| btd_profile_register(data); |
| adapter_add_profile(adapter, data); |
| } |
| |
| static struct external_profile *create_profile(struct gatt_app *app, |
| GDBusProxy *proxy, |
| const char *path) |
| { |
| struct external_profile *profile; |
| DBusMessageIter iter, array; |
| |
| if (!path || !g_str_has_prefix(path, "/")) |
| return NULL; |
| |
| profile = new0(struct external_profile, 1); |
| |
| profile->app = app; |
| profile->proxy = g_dbus_proxy_ref(proxy); |
| profile->profiles = queue_new(); |
| |
| if (!g_dbus_proxy_get_property(proxy, "UUIDs", &iter)) { |
| DBG("UUIDs property not found"); |
| goto fail; |
| } |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { |
| DBG("UUIDs wrongly formatted"); |
| goto fail; |
| } |
| |
| dbus_message_iter_recurse(&iter, &array); |
| |
| while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_STRING) { |
| const char *uuid; |
| |
| dbus_message_iter_get_basic(&array, &uuid); |
| |
| if (profile_add(profile, uuid) < 0) |
| goto fail; |
| |
| dbus_message_iter_next(&array); |
| } |
| |
| if (queue_isempty(profile->profiles)) |
| goto fail; |
| |
| queue_foreach(profile->profiles, add_profile, app->database->adapter); |
| queue_push_tail(app->profiles, profile); |
| |
| return profile; |
| |
| fail: |
| profile_free(profile); |
| return NULL; |
| } |
| |
| static void register_profile(void *data, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| GDBusProxy *proxy = data; |
| const char *iface = g_dbus_proxy_get_interface(proxy); |
| const char *path = g_dbus_proxy_get_path(proxy); |
| |
| if (app->failed) |
| return; |
| |
| if (g_strcmp0(iface, GATT_PROFILE_IFACE) == 0) { |
| struct external_profile *profile; |
| |
| profile = create_profile(app, proxy, path); |
| if (!profile) { |
| app->failed = true; |
| return; |
| } |
| } |
| } |
| |
| static void register_service(void *data, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| GDBusProxy *proxy = data; |
| const char *iface = g_dbus_proxy_get_interface(proxy); |
| const char *path = g_dbus_proxy_get_path(proxy); |
| |
| if (app->failed) |
| return; |
| |
| if (g_strcmp0(iface, GATT_SERVICE_IFACE) == 0) { |
| struct external_service *service; |
| |
| service = create_service(app, proxy, path); |
| if (!service) { |
| app->failed = true; |
| return; |
| } |
| } |
| } |
| |
| static void register_characteristic(void *data, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| GDBusProxy *proxy = data; |
| const char *iface; |
| const char *path; |
| |
| if (app->failed) |
| return; |
| |
| iface = g_dbus_proxy_get_interface(proxy); |
| path = g_dbus_proxy_get_path(proxy); |
| |
| if (g_strcmp0(iface, GATT_CHRC_IFACE) == 0) { |
| struct external_chrc *chrc; |
| |
| chrc = chrc_create(app, proxy, path); |
| if (!chrc) { |
| app->failed = true; |
| return; |
| } |
| } |
| } |
| |
| static void register_descriptor(void *data, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| GDBusProxy *proxy = data; |
| const char *iface = g_dbus_proxy_get_interface(proxy); |
| |
| if (app->failed) |
| return; |
| |
| if (g_strcmp0(iface, GATT_DESC_IFACE) == 0) { |
| struct external_desc *desc; |
| |
| desc = desc_create(app, proxy); |
| if (!desc) { |
| app->failed = true; |
| return; |
| } |
| } |
| } |
| |
| static void client_ready_cb(GDBusClient *client, void *user_data) |
| { |
| struct gatt_app *app = user_data; |
| DBusMessage *reply; |
| bool fail = false; |
| |
| /* |
| * Process received objects |
| */ |
| if (queue_isempty(app->proxies)) { |
| error("No object received"); |
| fail = true; |
| reply = btd_error_failed(app->reg, |
| "No object received"); |
| goto reply; |
| } |
| |
| queue_foreach(app->proxies, register_profile, app); |
| queue_foreach(app->proxies, register_service, app); |
| queue_foreach(app->proxies, register_characteristic, app); |
| queue_foreach(app->proxies, register_descriptor, app); |
| |
| if ((queue_isempty(app->services) && queue_isempty(app->profiles)) || |
| app->failed) { |
| error("No valid external GATT objects found"); |
| fail = true; |
| reply = btd_error_failed(app->reg, |
| "No valid service object found"); |
| goto reply; |
| } |
| |
| if (!database_add_app(app)) { |
| error("Failed to create GATT service entry in local database"); |
| fail = true; |
| reply = btd_error_failed(app->reg, |
| "Failed to create entry in database"); |
| goto reply; |
| } |
| |
| DBG("GATT application registered: %s:%s", app->owner, app->path); |
| |
| reply = dbus_message_new_method_return(app->reg); |
| |
| reply: |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| dbus_message_unref(app->reg); |
| app->reg = NULL; |
| |
| if (fail) |
| remove_app(app); |
| } |
| |
| static struct gatt_app *create_app(DBusConnection *conn, DBusMessage *msg, |
| const char *path) |
| { |
| struct gatt_app *app; |
| const char *sender = dbus_message_get_sender(msg); |
| |
| if (!path || !g_str_has_prefix(path, "/")) |
| return NULL; |
| |
| app = new0(struct gatt_app, 1); |
| |
| app->client = g_dbus_client_new_full(conn, sender, path, path); |
| if (!app->client) |
| goto fail; |
| |
| app->owner = g_strdup(sender); |
| if (!app->owner) |
| goto fail; |
| |
| app->path = g_strdup(path); |
| if (!app->path) |
| goto fail; |
| |
| app->services = queue_new(); |
| app->profiles = queue_new(); |
| app->proxies = queue_new(); |
| app->reg = dbus_message_ref(msg); |
| |
| g_dbus_client_set_disconnect_watch(app->client, client_disconnect_cb, |
| app); |
| g_dbus_client_set_proxy_handlers(app->client, proxy_added_cb, |
| proxy_removed_cb, NULL, app); |
| g_dbus_client_set_ready_watch(app->client, client_ready_cb, app); |
| |
| return app; |
| |
| fail: |
| app_free(app); |
| return NULL; |
| } |
| |
| static DBusMessage *manager_register_app(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| const char *sender = dbus_message_get_sender(msg); |
| DBusMessageIter args; |
| const char *path; |
| struct gatt_app *app; |
| struct svc_match_data match_data; |
| |
| if (!dbus_message_iter_init(msg, &args)) |
| return btd_error_invalid_args(msg); |
| |
| if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_OBJECT_PATH) |
| return btd_error_invalid_args(msg); |
| |
| dbus_message_iter_get_basic(&args, &path); |
| |
| match_data.path = path; |
| match_data.sender = sender; |
| |
| if (queue_find(database->apps, match_app, &match_data)) |
| return btd_error_already_exists(msg); |
| |
| dbus_message_iter_next(&args); |
| if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_ARRAY) |
| return btd_error_invalid_args(msg); |
| |
| app = create_app(conn, msg, path); |
| if (!app) |
| return btd_error_failed(msg, "Failed to register application"); |
| |
| DBG("Registering application: %s:%s", sender, path); |
| |
| app->database = database; |
| queue_push_tail(database->apps, app); |
| |
| return NULL; |
| } |
| |
| static DBusMessage *manager_unregister_app(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| const char *sender = dbus_message_get_sender(msg); |
| const char *path; |
| DBusMessageIter args; |
| struct gatt_app *app; |
| struct svc_match_data match_data; |
| |
| if (!dbus_message_iter_init(msg, &args)) |
| return btd_error_invalid_args(msg); |
| |
| if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_OBJECT_PATH) |
| return btd_error_invalid_args(msg); |
| |
| dbus_message_iter_get_basic(&args, &path); |
| |
| match_data.path = path; |
| match_data.sender = sender; |
| |
| app = queue_remove_if(database->apps, match_app, &match_data); |
| if (!app) |
| return btd_error_does_not_exist(msg); |
| |
| app_free(app); |
| |
| return dbus_message_new_method_return(msg); |
| } |
| |
| static const GDBusMethodTable manager_methods[] = { |
| { GDBUS_ASYNC_METHOD("RegisterApplication", |
| GDBUS_ARGS({ "application", "o" }, |
| { "options", "a{sv}" }), |
| NULL, manager_register_app) }, |
| { GDBUS_ASYNC_METHOD("UnregisterApplication", |
| GDBUS_ARGS({ "application", "o" }), |
| NULL, manager_unregister_app) }, |
| { } |
| }; |
| |
| static uint8_t server_authorize(struct bt_att *att, uint8_t opcode, |
| uint16_t handle, void *user_data) |
| { |
| struct btd_gatt_database *database = user_data; |
| struct device_state *state; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| |
| if (!get_dst_info(att, &bdaddr, &bdaddr_type)) |
| return 0; |
| |
| /* Skip if there is no device state */ |
| state = find_device_state(database, &bdaddr, bdaddr_type); |
| if (!state) |
| return 0; |
| |
| /* Skip if client doesn't support Robust Caching */ |
| if (!(state->cli_feat[0] & BT_GATT_CHRC_CLI_FEAT_ROBUST_CACHING)) |
| return 0; |
| |
| if (state->change_aware) |
| return 0; |
| |
| if (state->out_of_sync) { |
| state->out_of_sync = false; |
| state->change_aware = true; |
| return 0; |
| } |
| |
| state->out_of_sync = true; |
| |
| return BT_ATT_ERROR_DB_OUT_OF_SYNC; |
| } |
| |
| static void eatt_confirm_cb(GIOChannel *io, gpointer data) |
| { |
| char address[18]; |
| uint8_t dst_type; |
| bdaddr_t src, dst; |
| GError *gerr = NULL; |
| struct btd_device *device; |
| struct bt_gatt_server *server; |
| struct bt_att *att; |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_DEST_TYPE, &dst_type, |
| BT_IO_OPT_DEST, address, |
| BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| goto drop; |
| } |
| |
| DBG("New incoming EATT connection"); |
| |
| /* Confirm the device exists before accepting the connection, if the |
| * device is using an RPA it could be that the MGMT event has not been |
| * processed yet which would lead to create a second copy of the same |
| * device using its identity address. |
| */ |
| device = btd_adapter_find_device(adapter_find(&src), &dst, dst_type); |
| if (!device) { |
| error("Unable to find device: %s", address); |
| goto drop; |
| } |
| |
| /* Only allow EATT connection from central */ |
| if (btd_device_is_initiator(device)) { |
| warn("EATT connection from peripheral may cause collisions"); |
| goto drop; |
| } |
| |
| server = btd_device_get_gatt_server(device); |
| if (!server) { |
| error("Unable to resolve bt_server"); |
| goto drop; |
| } |
| |
| att = bt_gatt_server_get_att(server); |
| if (bt_att_get_channels(att) == btd_opts.gatt_channels) { |
| DBG("EATT channel limit reached"); |
| goto drop; |
| } |
| |
| if (!bt_io_accept(io, connect_cb, NULL, NULL, &gerr)) { |
| error("bt_io_accept: %s", gerr->message); |
| g_error_free(gerr); |
| goto drop; |
| } |
| |
| return; |
| |
| drop: |
| g_io_channel_shutdown(io, TRUE, NULL); |
| } |
| |
| struct btd_gatt_database *btd_gatt_database_new(struct btd_adapter *adapter) |
| { |
| struct btd_gatt_database *database; |
| GError *gerr = NULL; |
| const bdaddr_t *addr; |
| |
| if (!adapter) |
| return NULL; |
| |
| database = new0(struct btd_gatt_database, 1); |
| database->adapter = btd_adapter_ref(adapter); |
| database->db = gatt_db_new(); |
| database->records = queue_new(); |
| database->device_states = queue_new(); |
| database->apps = queue_new(); |
| database->profiles = queue_new(); |
| database->ccc_callbacks = queue_new(); |
| |
| addr = btd_adapter_get_address(adapter); |
| database->le_io = bt_io_listen(connect_cb, NULL, NULL, NULL, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, addr, |
| BT_IO_OPT_SOURCE_TYPE, |
| btd_adapter_get_address_type(adapter), |
| BT_IO_OPT_CID, BT_ATT_CID, |
| BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_LOW, |
| BT_IO_OPT_INVALID); |
| if (!database->le_io) { |
| error("Failed to start listening: %s", gerr->message); |
| g_error_free(gerr); |
| goto fail; |
| } |
| |
| /* If just just 1 channel is enabled EATT is not required */ |
| if (btd_opts.gatt_channels == 1) |
| goto bredr; |
| |
| /* EATT socket, encryption is required */ |
| database->eatt_io = bt_io_listen(NULL, eatt_confirm_cb, NULL, NULL, |
| &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, addr, |
| BT_IO_OPT_SOURCE_TYPE, |
| btd_adapter_get_address_type(adapter), |
| BT_IO_OPT_PSM, BT_ATT_EATT_PSM, |
| BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, |
| BT_IO_OPT_MTU, btd_opts.gatt_mtu, |
| BT_IO_OPT_INVALID); |
| if (!database->eatt_io) { |
| g_error_free(gerr); |
| goto fail; |
| } |
| |
| bredr: |
| /* BR/EDR socket */ |
| if (btd_adapter_get_bredr(adapter)) { |
| database->bredr_io = bt_io_listen(connect_cb, NULL, NULL, NULL, |
| &gerr, BT_IO_OPT_SOURCE_BDADDR, addr, |
| BT_IO_OPT_PSM, BT_ATT_PSM, |
| BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, |
| BT_IO_OPT_MTU, btd_opts.gatt_mtu, |
| BT_IO_OPT_INVALID); |
| if (database->bredr_io == NULL) { |
| error("Failed to start listening: %s", gerr->message); |
| g_error_free(gerr); |
| goto fail; |
| } |
| } |
| |
| if (g_dbus_register_interface(btd_get_dbus_connection(), |
| adapter_get_path(adapter), |
| GATT_MANAGER_IFACE, |
| manager_methods, NULL, NULL, |
| database, NULL)) |
| DBG("GATT Manager registered for adapter: %s", |
| adapter_get_path(adapter)); |
| |
| register_core_services(database); |
| |
| database->db_id = gatt_db_register(database->db, gatt_db_service_added, |
| gatt_db_service_removed, |
| database, NULL); |
| if (!database->db_id) |
| goto fail; |
| |
| if (!dbs) |
| dbs = queue_new(); |
| |
| queue_push_tail(dbs, database); |
| |
| return database; |
| |
| fail: |
| gatt_database_free(database); |
| |
| return NULL; |
| } |
| |
| void btd_gatt_database_destroy(struct btd_gatt_database *database) |
| { |
| if (!database) |
| return; |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), |
| adapter_get_path(database->adapter), |
| GATT_MANAGER_IFACE); |
| |
| gatt_database_free(database); |
| } |
| |
| static bool match_db(const void *data, const void *user_data) |
| { |
| const struct btd_gatt_database *database = data; |
| const struct gatt_db *db = user_data; |
| |
| return database->db == db; |
| } |
| |
| struct btd_gatt_database *btd_gatt_database_get(struct gatt_db *db) |
| { |
| struct btd_gatt_database *database; |
| |
| database = queue_find(dbs, match_db, db); |
| if (!database) |
| return NULL; |
| |
| return database; |
| } |
| |
| struct btd_adapter * |
| btd_gatt_database_get_adapter(struct btd_gatt_database *database) |
| { |
| if (!database) |
| return NULL; |
| |
| return database->adapter; |
| } |
| |
| struct gatt_db *btd_gatt_database_get_db(struct btd_gatt_database *database) |
| { |
| if (!database) |
| return NULL; |
| |
| return database->db; |
| } |
| |
| void btd_gatt_database_server_connected(struct btd_gatt_database *database, |
| struct bt_gatt_server *server) |
| { |
| struct bt_att *att = bt_gatt_server_get_att(server); |
| struct device_state *state; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| |
| if (!get_dst_info(att, &bdaddr, &bdaddr_type)) |
| return; |
| |
| bt_gatt_server_set_authorize(server, server_authorize, database); |
| |
| state = find_device_state(database, &bdaddr, bdaddr_type); |
| if (!state || !state->pending) |
| return; |
| |
| send_notification_to_device(state, state->pending); |
| |
| state = find_device_state(database, &bdaddr, bdaddr_type); |
| if (!state || !state->pending) |
| return; |
| |
| free(state->pending->value); |
| free(state->pending); |
| state->pending = NULL; |
| } |
| |
| void btd_gatt_database_att_disconnected(struct btd_gatt_database *database, |
| struct btd_device *device) |
| { |
| struct bt_gatt_server *server = btd_device_get_gatt_server(device); |
| struct bt_att *att = bt_gatt_server_get_att(server); |
| struct device_state *state; |
| const bdaddr_t *addr; |
| uint8_t type; |
| |
| DBG(""); |
| |
| addr = device_get_address(device); |
| type = btd_device_get_bdaddr_type(device); |
| |
| state = find_device_state(database, addr, type); |
| if (!state) |
| return; |
| |
| if (state->disc_id) |
| bt_att_unregister_disconnect(att, state->disc_id); |
| |
| att_disconnected(0, state); |
| } |
| |
| static void restore_ccc(struct btd_gatt_database *database, |
| const bdaddr_t *addr, uint8_t addr_type, uint16_t value) |
| { |
| struct device_state *dev_state; |
| struct ccc_state *ccc; |
| |
| dev_state = device_state_create(database, addr, addr_type); |
| queue_push_tail(database->device_states, dev_state); |
| |
| ccc = new0(struct ccc_state, 1); |
| ccc->handle = gatt_db_attribute_get_handle(database->svc_chngd_ccc); |
| ccc->value = value; |
| queue_push_tail(dev_state->ccc_states, ccc); |
| } |
| |
| static void restore_state(struct btd_device *device, void *data) |
| { |
| struct btd_gatt_database *database = data; |
| uint16_t ccc_le, ccc_bredr; |
| |
| device_load_svc_chng_ccc(device, &ccc_le, &ccc_bredr); |
| |
| if (ccc_le) { |
| restore_ccc(database, device_get_address(device), |
| device_get_le_address_type(device), ccc_le); |
| |
| DBG("%s LE", device_get_path(device)); |
| } |
| |
| if (ccc_bredr) { |
| restore_ccc(database, device_get_address(device), |
| BDADDR_BREDR, ccc_bredr); |
| |
| DBG("%s BR/EDR", device_get_path(device)); |
| } |
| } |
| |
| void btd_gatt_database_restore_svc_chng_ccc(struct btd_gatt_database *database) |
| { |
| uint8_t value[4]; |
| uint16_t handle, ccc_handle; |
| |
| if (!database) |
| return; |
| |
| handle = gatt_db_attribute_get_handle(database->svc_chngd); |
| ccc_handle = gatt_db_attribute_get_handle(database->svc_chngd_ccc); |
| |
| if (!handle || !ccc_handle) { |
| error("Failed to obtain handles for \"Service Changed\"" |
| " characteristic"); |
| return; |
| } |
| |
| /* restore states for bonded device that registered for Service Changed |
| * indication |
| */ |
| btd_adapter_for_each_device(database->adapter, restore_state, database); |
| |
| put_le16(0x0001, value); |
| put_le16(0xffff, value + 2); |
| |
| if (!gatt_db_attribute_notify(database->svc_chngd, value, sizeof(value), |
| NULL)) |
| error("Failed to notify Service Changed"); |
| } |