| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2015 Google Inc. |
| * |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <unistd.h> |
| |
| #include "lib/bluetooth.h" |
| #include "lib/sdp.h" |
| #include "lib/sdp_lib.h" |
| #include "lib/uuid.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 "adapter.h" |
| #include "device.h" |
| #include "gatt-database.h" |
| #include "dbus-common.h" |
| #include "profile.h" |
| #include "service.h" |
| |
| #ifndef ATT_CID |
| #define ATT_CID 4 |
| #endif |
| |
| #ifndef ATT_PSM |
| #define ATT_PSM 31 |
| #endif |
| |
| #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 UUID_GAP 0x1800 |
| #define UUID_GATT 0x1801 |
| |
| #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 *l2cap_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 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 external_chrc { |
| struct external_service *service; |
| char *path; |
| GDBusProxy *proxy; |
| uint8_t props; |
| uint8_t ext_props; |
| uint32_t perm; |
| uint16_t mtu; |
| struct io *write_io; |
| struct io *notify_io; |
| 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 btd_device *device; |
| unsigned int 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; |
| }; |
| |
| struct device_state { |
| struct btd_gatt_database *db; |
| bdaddr_t bdaddr; |
| uint8_t bdaddr_type; |
| unsigned int disc_id; |
| 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; |
| uint8_t value[2]; |
| }; |
| |
| 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 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, 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[0]) |
| 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; |
| |
| device = btd_adapter_get_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) |
| device_store_svc_chng_ccc(device, state->bdaddr_type, |
| ccc->value[0]); |
| |
| 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; |
| |
| io = g_io_channel_unix_new(bt_att_get_fd(att)); |
| 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 *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 chrc_free(void *data) |
| { |
| struct external_chrc *chrc = data; |
| |
| io_destroy(chrc->write_io); |
| io_destroy(chrc->notify_io); |
| |
| 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); |
| |
| 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->l2cap_io) { |
| g_io_channel_shutdown(database->l2cap_io, FALSE, NULL); |
| g_io_channel_unref(database->l2cap_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); |
| |
| if (offset > 2) { |
| error = BT_ATT_ERROR_INVALID_OFFSET; |
| goto done; |
| } |
| |
| appearance[0] = dev_class & 0x00ff; |
| appearance[1] = (dev_class >> 8) & 0x001f; |
| |
| len -= offset; |
| value = len ? &appearance[offset] : NULL; |
| |
| done: |
| gatt_db_attribute_read_result(attrib, id, error, value, len); |
| } |
| |
| 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 = 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; |
| |
| /* Add the GAP service */ |
| bt_uuid16_create(&uuid, UUID_GAP); |
| service = gatt_db_add_service(database->db, &uuid, true, 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); |
| gatt_db_service_add_characteristic(service, &uuid, BT_ATT_PERM_READ, |
| BT_GATT_CHRC_PROP_READ, |
| gap_appearance_read_cb, |
| NULL, database); |
| |
| gatt_db_service_set_active(service, true); |
| } |
| |
| 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); |
| |
| 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; |
| } |
| |
| len = 2 - offset; |
| value = len ? &ccc->value[offset] : NULL; |
| |
| 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 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->device = device; |
| op->attrib = attrib; |
| op->link_type = link_type; |
| |
| return op; |
| } |
| |
| static void pending_op_free(void *data) |
| { |
| struct pending_op *op = data; |
| |
| if (op->owner_queue) |
| queue_remove(op->owner_queue, op); |
| |
| free(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; |
| 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; |
| } |
| |
| ccc_cb = queue_find(database->ccc_callbacks, ccc_cb_match_handle, |
| UINT_TO_PTR(gatt_db_attribute_get_handle(attrib))); |
| if (!ccc_cb) { |
| ecode = BT_ATT_ERROR_UNLIKELY; |
| goto done; |
| } |
| |
| /* If value is identical, then just succeed */ |
| if (ccc->value[0] == value[0] && ccc->value[1] == value[1]) |
| goto done; |
| |
| if (ccc_cb->callback) { |
| struct pending_op *op; |
| |
| op = pending_ccc_new(att, attrib, get_le16(value), |
| 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[0] = value[0]; |
| ccc->value[1] = value[1]; |
| } |
| |
| done: |
| gatt_db_attribute_write_result(attrib, id, ecode); |
| } |
| |
| 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, |
| btd_gatt_database_destroy_t destroy) |
| { |
| struct gatt_db_attribute *ccc; |
| struct ccc_cb_data *ccc_cb; |
| bt_uuid_t uuid; |
| |
| ccc_cb = new0(struct ccc_cb_data, 1); |
| |
| bt_uuid16_create(&uuid, GATT_CLIENT_CHARAC_CFG_UUID); |
| ccc = gatt_db_service_add_descriptor(service, &uuid, |
| BT_ATT_PERM_READ | BT_ATT_PERM_WRITE, |
| gatt_ccc_read_cb, gatt_ccc_write_cb, database); |
| if (!ccc) { |
| error("Failed to create CCC entry in database"); |
| free(ccc_cb); |
| return NULL; |
| } |
| |
| ccc_cb->handle = gatt_db_attribute_get_handle(ccc); |
| ccc_cb->callback = write_callback; |
| ccc_cb->destroy = destroy; |
| ccc_cb->user_data = user_data; |
| |
| queue_push_tail(database->ccc_callbacks, ccc_cb); |
| |
| return ccc; |
| } |
| |
| 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, 4); |
| |
| bt_uuid16_create(&uuid, GATT_CHARAC_SERVICE_CHANGED); |
| database->svc_chngd = gatt_db_service_add_characteristic(service, &uuid, |
| BT_ATT_PERM_READ, BT_GATT_CHRC_PROP_INDICATE, |
| NULL, NULL, database); |
| |
| database->svc_chngd_ccc = service_add_ccc(service, database, NULL, NULL, |
| NULL); |
| |
| gatt_db_service_set_active(service, true); |
| } |
| |
| static void register_core_services(struct btd_gatt_database *database) |
| { |
| populate_gap_service(database); |
| populate_gatt_service(database); |
| } |
| |
| 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) |
| { |
| DBG(""); |
| } |
| |
| 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; |
| |
| ccc = find_ccc_state(device_state, notify->ccc_handle); |
| if (!ccc) |
| return; |
| |
| if (!ccc->value[0] || (notify->conf && !(ccc->value[0] & 0x02))) |
| return; |
| |
| device = btd_adapter_get_device(notify->database->adapter, |
| &device_state->bdaddr, |
| device_state->bdaddr_type); |
| if (!device) |
| 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 (!notify->conf) { |
| DBG("GATT server sending notification"); |
| bt_gatt_server_send_notification(server, |
| notify->handle, notify->value, |
| notify->len); |
| 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 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); |
| |
| send_notification_to_devices(database, handle, value, sizeof(value), |
| ccc_handle, service_changed_conf, NULL); |
| } |
| |
| 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); |
| } |
| |
| 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, |
| bool *req_prep_authorization) |
| { |
| const char *flag; |
| |
| *props = *ext_props = 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; |
| } else if (!strcmp("indicate", flag)) { |
| *props |= BT_GATT_CHRC_PROP_INDICATE; |
| } else if (!strcmp("authenticated-signed-writes", flag)) { |
| *props |= BT_GATT_CHRC_PROP_AUTH; |
| *perm |= BT_ATT_PERM_WRITE; |
| } 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; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_ENC_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_ENCRYPT; |
| } else if (!strcmp("encrypt-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_ENC_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_ENCRYPT; |
| } else if (!strcmp("encrypt-authenticated-read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_AUTH_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_AUTHEN; |
| } else if (!strcmp("encrypt-authenticated-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_AUTH_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_AUTHEN; |
| } else if (!strcmp("secure-read", flag)) { |
| *props |= BT_GATT_CHRC_PROP_READ; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_AUTH_READ; |
| *perm |= BT_ATT_PERM_READ | BT_ATT_PERM_READ_SECURE; |
| } else if (!strcmp("secure-write", flag)) { |
| *props |= BT_GATT_CHRC_PROP_WRITE; |
| *ext_props |= BT_GATT_CHRC_EXT_PROP_AUTH_WRITE; |
| *perm |= BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_SECURE; |
| } else if (!strcmp("authorize", flag)) { |
| *req_prep_authorization = true; |
| } 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, 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, |
| 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->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, |
| &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; |
| |
| /* 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); |
| |
| do { |
| if (dbus_message_iter_get_arg_type(&array) != |
| DBUS_TYPE_OBJECT_PATH) |
| return false; |
| |
| dbus_message_iter_get_basic(&array, &obj); |
| |
| if (!queue_push_tail(service->includes, obj)) { |
| error("Failed to add Includes path in queue\n"); |
| return false; |
| |
| } |
| |
| incr_attr_count(service, 1); |
| } while (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 uint8_t dbus_error_to_att_ecode(const char *error_name) |
| { |
| |
| if (strcmp(error_name, "org.bluez.Error.Failed") == 0) |
| return 0x80; /* For now return this "application error" */ |
| |
| if (strcmp(error_name, "org.bluez.Error.NotSupported") == 0) |
| return BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| |
| if (strcmp(error_name, "org.bluez.Error.NotAuthorized") == 0) |
| return BT_ATT_ERROR_AUTHORIZATION; |
| |
| if (strcmp(error_name, "org.bluez.Error.InvalidValueLength") == 0) |
| return BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN; |
| |
| if (strcmp(error_name, "org.bluez.Error.InvalidOffset") == 0) |
| return BT_ATT_ERROR_INVALID_OFFSET; |
| |
| if (strcmp(error_name, "org.bluez.Error.InProgress") == 0) |
| return BT_ERROR_ALREADY_IN_PROGRESS; |
| |
| return 0; |
| } |
| |
| 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); |
| ecode = ecode ? ecode : 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 btd_device *device, |
| struct queue *owner_queue, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t link_type) |
| { |
| struct pending_op *op; |
| |
| op = new0(struct pending_op, 1); |
| |
| op->owner_queue = owner_queue; |
| op->device = device; |
| op->attrib = attrib; |
| op->id = id; |
| op->offset = offset; |
| op->link_type = link_type; |
| queue_push_tail(owner_queue, op); |
| |
| return op; |
| } |
| |
| static void append_options(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| const char *path = device_get_path(op->device); |
| const char *link; |
| |
| switch (op->link_type) { |
| case BT_ATT_LINK_BREDR: |
| link = "BR/EDR"; |
| break; |
| case BT_ATT_LINK_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); |
| } |
| |
| 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 btd_device *device, |
| struct gatt_db_attribute *attrib, |
| GDBusProxy *proxy, |
| struct queue *owner_queue, |
| unsigned int id, |
| uint16_t offset, |
| uint8_t link_type) |
| { |
| struct pending_op *op; |
| |
| op = pending_read_new(device, owner_queue, attrib, id, offset, |
| link_type); |
| |
| 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); |
| ecode = ecode ? ecode : 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 btd_device *device, |
| struct queue *owner_queue, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, |
| const uint8_t *value, size_t len, |
| uint16_t offset, uint8_t link_type, |
| 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->device = device; |
| op->owner_queue = owner_queue; |
| op->attrib = attrib; |
| op->id = id; |
| op->offset = offset; |
| op->link_type = link_type; |
| op->is_characteristic = is_characteristic; |
| op->prep_authorize = prep_authorize; |
| queue_push_tail(owner_queue, op); |
| |
| return op; |
| } |
| |
| static struct pending_op *send_write(struct btd_device *device, |
| struct gatt_db_attribute *attrib, |
| GDBusProxy *proxy, |
| struct queue *owner_queue, |
| unsigned int id, |
| const uint8_t *value, size_t len, |
| uint16_t offset, uint8_t link_type, |
| bool is_characteristic, |
| bool prep_authorize) |
| { |
| struct pending_op *op; |
| |
| op = pending_write_new(device, owner_queue, attrib, id, value, len, |
| offset, link_type, 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 bool pipe_hup(struct io *io, void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| |
| DBG("%p closed\n", io); |
| |
| if (io == chrc->write_io) |
| chrc->write_io = NULL; |
| else |
| chrc->notify_io = NULL; |
| |
| io_destroy(io); |
| |
| return false; |
| } |
| |
| static bool pipe_io_read(struct io *io, void *user_data) |
| { |
| struct external_chrc *chrc = user_data; |
| uint8_t buf[512]; |
| int fd = io_get_fd(io); |
| ssize_t bytes_read; |
| |
| bytes_read = read(fd, buf, sizeof(buf)); |
| if (bytes_read < 0) |
| return false; |
| |
| send_notification_to_devices(chrc->service->app->database, |
| gatt_db_attribute_get_handle(chrc->attrib), |
| buf, bytes_read, |
| gatt_db_attribute_get_handle(chrc->ccc), |
| chrc->props & BT_GATT_CHRC_PROP_INDICATE ? |
| conf_cb : NULL, chrc->proxy); |
| |
| return true; |
| } |
| |
| static struct io *pipe_io_new(int fd, void *user_data) |
| { |
| struct io *io; |
| |
| io = io_new(fd); |
| |
| io_set_close_on_destroy(io, true); |
| |
| io_set_read_handler(io, pipe_io_read, user_data, NULL); |
| |
| io_set_disconnect_handler(io, pipe_hup, user_data, NULL); |
| |
| return io; |
| } |
| |
| static int pipe_io_send(struct io *io, const void *data, size_t len) |
| { |
| struct iovec iov; |
| |
| iov.iov_base = (void *) data; |
| iov.iov_len = len; |
| |
| return io_send(io, &iov, 1); |
| } |
| |
| static void acquire_write_reply(DBusMessage *message, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| struct external_chrc *chrc; |
| DBusError err; |
| int fd; |
| uint16_t mtu; |
| |
| chrc = gatt_db_attribute_get_user_data(op->attrib); |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| error("Failed to acquire write: %s\n", err.name); |
| 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 AcquireWrite response\n"); |
| goto retry; |
| } |
| |
| DBG("AcquireWrite success: fd %d MTU %u\n", fd, mtu); |
| |
| chrc->write_io = pipe_io_new(fd, chrc); |
| |
| if (pipe_io_send(chrc->write_io, op->data.iov_base, |
| op->data.iov_len) < 0) |
| goto retry; |
| |
| gatt_db_attribute_write_result(op->attrib, op->id, 0); |
| |
| return; |
| |
| retry: |
| send_write(op->device, op->attrib, chrc->proxy, NULL, op->id, |
| op->data.iov_base, op->data.iov_len, 0, |
| op->link_type, false, false); |
| } |
| |
| static void acquire_write_setup(DBusMessageIter *iter, void *user_data) |
| { |
| struct pending_op *op = user_data; |
| DBusMessageIter dict; |
| struct bt_gatt_server *server; |
| uint16_t mtu; |
| |
| 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); |
| |
| server = btd_device_get_gatt_server(op->device); |
| |
| mtu = bt_gatt_server_get_mtu(server); |
| |
| dict_append_entry(&dict, "MTU", DBUS_TYPE_UINT16, &mtu); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static struct pending_op *acquire_write(struct external_chrc *chrc, |
| struct btd_device *device, |
| struct gatt_db_attribute *attrib, |
| unsigned int id, |
| const uint8_t *value, size_t len, |
| uint8_t link_type) |
| { |
| struct pending_op *op; |
| |
| op = pending_write_new(device, NULL, attrib, id, value, len, 0, |
| link_type, false, false); |
| |
| if (g_dbus_proxy_method_call(chrc->proxy, "AcquireWrite", |
| acquire_write_setup, |
| acquire_write_reply, |
| op, pending_op_free)) |
| return op; |
| |
| pending_op_free(op); |
| |
| return NULL; |
| } |
| |
| 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; |
| DBusError err; |
| int fd; |
| uint16_t mtu; |
| |
| dbus_error_init(&err); |
| |
| if (dbus_set_error_from_message(&err, message) == TRUE) { |
| error("Failed to acquire notify: %s\n", err.name); |
| 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); |
| |
| chrc->notify_io = pipe_io_new(fd, chrc); |
| |
| __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; |
| struct bt_gatt_server *server; |
| uint16_t mtu; |
| |
| 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); |
| |
| server = btd_device_get_gatt_server(op->device); |
| |
| mtu = bt_gatt_server_get_mtu(server); |
| |
| dict_append_entry(&dict, "MTU", DBUS_TYPE_UINT16, &mtu); |
| |
| 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; |
| 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; |
| |
| if (__sync_sub_and_fetch(&chrc->ntfy_cnt, 1)) |
| goto done; |
| |
| if (chrc->notify_io) { |
| io_destroy(chrc->notify_io); |
| chrc->notify_io = NULL; |
| 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; |
| |
| if (chrc->notify_io) { |
| __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); |
| 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 (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), |
| chrc->props & BT_GATT_CHRC_PROP_INDICATE ? |
| conf_cb : NULL, 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; |
| |
| chrc->ccc = service_add_ccc(service->attrib, service->app->database, |
| ccc_write_cb, chrc, 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(device, attrib, desc->proxy, desc->pending_reads, id, |
| offset, bt_att_get_link_type(att))) |
| 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 (!device_is_trusted(device) && !desc->prep_authorized && |
| desc->req_prep_authorization) |
| send_write(device, attrib, desc->proxy, |
| desc->pending_writes, id, value, len, |
| offset, bt_att_get_link_type(att), |
| 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(device, attrib, desc->proxy, desc->pending_writes, id, |
| value, len, offset, bt_att_get_link_type(att), false, |
| false)) |
| return; |
| |
| fail: |
| gatt_db_attribute_write_result(attrib, id, BT_ATT_ERROR_UNLIKELY); |
| } |
| |
| static bool database_add_desc(struct external_service *service, |
| struct external_desc *desc) |
| { |
| bt_uuid_t uuid; |
| |
| if (!parse_uuid(desc->proxy, &uuid)) { |
| error("Failed to read \"UUID\" property of descriptor"); |
| return false; |
| } |
| |
| desc->attrib = gatt_db_service_add_descriptor(service->attrib, &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; |
| |
| 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(device, attrib, chrc->proxy, chrc->pending_reads, id, |
| offset, bt_att_get_link_type(att))) |
| 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 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 (!device_is_trusted(device) && !chrc->prep_authorized && |
| chrc->req_prep_authorization) |
| send_write(device, attrib, chrc->proxy, queue, |
| id, value, len, offset, |
| bt_att_get_link_type(att), true, true); |
| else |
| gatt_db_attribute_write_result(attrib, id, 0); |
| |
| return; |
| } |
| |
| if (opcode == BT_ATT_OP_EXEC_WRITE_REQ) |
| chrc->prep_authorized = false; |
| |
| if (chrc->write_io) { |
| if (pipe_io_send(chrc->write_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, device, attrib, id, value, len, |
| bt_att_get_link_type(att))) |
| return; |
| } |
| |
| if (send_write(device, attrib, chrc->proxy, queue, id, value, len, |
| offset, bt_att_get_link_type(att), 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) |
| { |
| bt_uuid_t uuid; |
| const struct queue_entry *entry; |
| |
| 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; |
| } |
| |
| chrc->attrib = gatt_db_service_add_characteristic(service->attrib, |
| &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; |
| |
| /* 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; |
| const struct queue_entry *entry; |
| |
| 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; |
| } |
| |
| service->attrib = gatt_db_add_service(service->app->database->db, &uuid, |
| primary, service->attr_cnt); |
| if (!service->attrib) |
| return false; |
| |
| 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; |
| } |
| |
| 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 = g_dbus_proxy_get_interface(proxy); |
| const char *path = g_dbus_proxy_get_path(proxy); |
| |
| 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_fo
|