| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2010 Nokia Corporation |
| * Copyright (C) 2010 Marcel Holtmann <marcel@holtmann.org> |
| * |
| * |
| * 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. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <errno.h> |
| #include <stdint.h> |
| #include <stdbool.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <glib.h> |
| #include <sys/stat.h> |
| |
| #include "lib/bluetooth.h" |
| #include "lib/sdp.h" |
| #include "lib/sdp_lib.h" |
| #include "lib/uuid.h" |
| |
| #include "btio/btio.h" |
| #include "log.h" |
| #include "backtrace.h" |
| #include "adapter.h" |
| #include "device.h" |
| #include "src/shared/util.h" |
| #include "attrib/gattrib.h" |
| #include "attrib/att.h" |
| #include "attrib/gatt.h" |
| #include "attrib/att-database.h" |
| #include "textfile.h" |
| #include "storage.h" |
| |
| #include "attrib-server.h" |
| |
| static GSList *servers = NULL; |
| |
| struct gatt_server { |
| struct btd_adapter *adapter; |
| GIOChannel *l2cap_io; |
| GIOChannel *le_io; |
| uint32_t gatt_sdp_handle; |
| uint32_t gap_sdp_handle; |
| GList *database; |
| GSList *clients; |
| uint16_t name_handle; |
| uint16_t appearance_handle; |
| }; |
| |
| struct gatt_channel { |
| GAttrib *attrib; |
| guint mtu; |
| gboolean le; |
| guint id; |
| gboolean encrypted; |
| struct gatt_server *server; |
| guint cleanup_id; |
| struct btd_device *device; |
| }; |
| |
| struct group_elem { |
| uint16_t handle; |
| uint16_t end; |
| uint8_t *data; |
| uint16_t len; |
| }; |
| |
| static bt_uuid_t prim_uuid = { |
| .type = BT_UUID16, |
| .value.u16 = GATT_PRIM_SVC_UUID |
| }; |
| static bt_uuid_t snd_uuid = { |
| .type = BT_UUID16, |
| .value.u16 = GATT_SND_SVC_UUID |
| }; |
| static bt_uuid_t ccc_uuid = { |
| .type = BT_UUID16, |
| .value.u16 = GATT_CLIENT_CHARAC_CFG_UUID |
| }; |
| |
| static void attrib_free(void *data) |
| { |
| struct attribute *a = data; |
| |
| g_free(a->data); |
| g_free(a); |
| } |
| |
| static void channel_free(struct gatt_channel *channel) |
| { |
| |
| if (channel->cleanup_id) |
| g_source_remove(channel->cleanup_id); |
| |
| if (channel->device) |
| btd_device_unref(channel->device); |
| |
| g_attrib_unref(channel->attrib); |
| g_free(channel); |
| } |
| |
| static void gatt_server_free(struct gatt_server *server) |
| { |
| g_list_free_full(server->database, attrib_free); |
| |
| if (server->l2cap_io != NULL) { |
| g_io_channel_shutdown(server->l2cap_io, FALSE, NULL); |
| g_io_channel_unref(server->l2cap_io); |
| } |
| |
| if (server->le_io != NULL) { |
| g_io_channel_shutdown(server->le_io, FALSE, NULL); |
| g_io_channel_unref(server->le_io); |
| } |
| |
| g_slist_free_full(server->clients, (GDestroyNotify) channel_free); |
| |
| if (server->gatt_sdp_handle > 0) |
| adapter_service_remove(server->adapter, |
| server->gatt_sdp_handle); |
| |
| if (server->gap_sdp_handle > 0) |
| adapter_service_remove(server->adapter, server->gap_sdp_handle); |
| |
| if (server->adapter != NULL) |
| btd_adapter_unref(server->adapter); |
| |
| g_free(server); |
| } |
| |
| static int adapter_cmp_addr(gconstpointer a, gconstpointer b) |
| { |
| const struct gatt_server *server = a; |
| const bdaddr_t *bdaddr = b; |
| |
| return bacmp(btd_adapter_get_address(server->adapter), bdaddr); |
| } |
| |
| static int adapter_cmp(gconstpointer a, gconstpointer b) |
| { |
| const struct gatt_server *server = a; |
| const struct btd_adapter *adapter = b; |
| |
| if (server->adapter == adapter) |
| return 0; |
| |
| return -1; |
| } |
| |
| static struct gatt_server *find_gatt_server(const bdaddr_t *bdaddr) |
| { |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, bdaddr, adapter_cmp_addr); |
| if (l == NULL) { |
| char addr[18]; |
| |
| ba2str(bdaddr, addr); |
| error("No GATT server found in %s", addr); |
| return NULL; |
| } |
| |
| return l->data; |
| } |
| |
| static sdp_record_t *server_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 int handle_cmp(gconstpointer a, gconstpointer b) |
| { |
| const struct attribute *attrib = a; |
| uint16_t handle = GPOINTER_TO_UINT(b); |
| |
| return attrib->handle - handle; |
| } |
| |
| static int attribute_cmp(gconstpointer a1, gconstpointer a2) |
| { |
| const struct attribute *attrib1 = a1; |
| const struct attribute *attrib2 = a2; |
| |
| return attrib1->handle - attrib2->handle; |
| } |
| |
| static struct attribute *find_svc_range(struct gatt_server *server, |
| uint16_t start, uint16_t *end) |
| { |
| struct attribute *attrib; |
| guint h = start; |
| GList *l; |
| |
| if (end == NULL) |
| return NULL; |
| |
| l = g_list_find_custom(server->database, GUINT_TO_POINTER(h), |
| handle_cmp); |
| if (!l) |
| return NULL; |
| |
| attrib = l->data; |
| |
| if (bt_uuid_cmp(&attrib->uuid, &prim_uuid) != 0 && |
| bt_uuid_cmp(&attrib->uuid, &snd_uuid) != 0) |
| return NULL; |
| |
| *end = start; |
| |
| for (l = l->next; l; l = l->next) { |
| struct attribute *a = l->data; |
| |
| if (bt_uuid_cmp(&a->uuid, &prim_uuid) == 0 || |
| bt_uuid_cmp(&a->uuid, &snd_uuid) == 0) |
| break; |
| |
| *end = a->handle; |
| } |
| |
| return attrib; |
| } |
| |
| static uint32_t attrib_create_sdp_new(struct gatt_server *server, |
| uint16_t handle, const char *name) |
| { |
| sdp_record_t *record; |
| struct attribute *a; |
| uint16_t end = 0; |
| uuid_t svc, gap_uuid; |
| |
| a = find_svc_range(server, handle, &end); |
| |
| if (a == NULL) |
| return 0; |
| |
| if (a->len == 2) |
| sdp_uuid16_create(&svc, get_le16(a->data)); |
| else if (a->len == 16) { |
| uint8_t be128[16]; |
| |
| /* Converting from LE to BE */ |
| bswap_128(a->data, be128); |
| sdp_uuid128_create(&svc, be128); |
| } else |
| return 0; |
| |
| record = server_record_new(&svc, handle, end); |
| if (record == NULL) |
| return 0; |
| |
| if (name != NULL) |
| sdp_set_info_attr(record, name, "BlueZ", NULL); |
| |
| sdp_uuid16_create(&gap_uuid, GENERIC_ACCESS_PROFILE_ID); |
| 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(server->adapter, record) == 0) |
| return record->handle; |
| |
| sdp_record_free(record); |
| return 0; |
| } |
| |
| static struct attribute *attrib_db_add_new(struct gatt_server *server, |
| uint16_t handle, bt_uuid_t *uuid, |
| int read_req, int write_req, |
| const uint8_t *value, size_t len) |
| { |
| struct attribute *a; |
| guint h = handle; |
| |
| DBG("handle=0x%04x", handle); |
| |
| if (g_list_find_custom(server->database, GUINT_TO_POINTER(h), |
| handle_cmp)) |
| return NULL; |
| |
| a = g_new0(struct attribute, 1); |
| a->len = len; |
| a->data = g_memdup(value, len); |
| a->handle = handle; |
| a->uuid = *uuid; |
| a->read_req = read_req; |
| a->write_req = write_req; |
| |
| server->database = g_list_insert_sorted(server->database, a, |
| attribute_cmp); |
| |
| return a; |
| } |
| |
| static bool g_attrib_is_encrypted(GAttrib *attrib) |
| { |
| BtIOSecLevel sec_level; |
| GIOChannel *io = g_attrib_get_channel(attrib); |
| |
| if (!bt_io_get(io, NULL, BT_IO_OPT_SEC_LEVEL, &sec_level, |
| BT_IO_OPT_INVALID)) |
| return FALSE; |
| |
| return sec_level > BT_IO_SEC_LOW; |
| } |
| |
| static uint8_t att_check_reqs(struct gatt_channel *channel, uint8_t opcode, |
| int reqs) |
| { |
| /* FIXME: currently, it is assumed an encrypted link is enough for |
| * authentication. This will allow to enable the SMP negotiation once |
| * it is on upstream kernel. High security level should be mapped |
| * to authentication and medium to encryption permission. */ |
| if (!channel->encrypted) |
| channel->encrypted = g_attrib_is_encrypted(channel->attrib); |
| if (reqs == ATT_AUTHENTICATION && !channel->encrypted) |
| return ATT_ECODE_AUTHENTICATION; |
| else if (reqs == ATT_AUTHORIZATION) |
| return ATT_ECODE_AUTHORIZATION; |
| |
| switch (opcode) { |
| case ATT_OP_READ_BY_GROUP_REQ: |
| case ATT_OP_READ_BY_TYPE_REQ: |
| case ATT_OP_READ_REQ: |
| case ATT_OP_READ_BLOB_REQ: |
| case ATT_OP_READ_MULTI_REQ: |
| if (reqs == ATT_NOT_PERMITTED) |
| return ATT_ECODE_READ_NOT_PERM; |
| break; |
| case ATT_OP_PREP_WRITE_REQ: |
| case ATT_OP_WRITE_REQ: |
| case ATT_OP_WRITE_CMD: |
| if (reqs == ATT_NOT_PERMITTED) |
| return ATT_ECODE_WRITE_NOT_PERM; |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static uint16_t read_by_group(struct gatt_channel *channel, uint16_t start, |
| uint16_t end, bt_uuid_t *uuid, |
| uint8_t *pdu, size_t len) |
| { |
| struct att_data_list *adl; |
| struct attribute *a; |
| struct group_elem *cur, *old = NULL; |
| GSList *l, *groups; |
| GList *dl, *database; |
| uint16_t length, last_handle, last_size = 0; |
| uint8_t status; |
| int i; |
| |
| if (start > end || start == 0x0000) |
| return enc_error_resp(ATT_OP_READ_BY_GROUP_REQ, start, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| /* |
| * Only <<Primary Service>> and <<Secondary Service>> grouping |
| * types may be used in the Read By Group Type Request. |
| */ |
| |
| if (bt_uuid_cmp(uuid, &prim_uuid) != 0 && |
| bt_uuid_cmp(uuid, &snd_uuid) != 0) |
| return enc_error_resp(ATT_OP_READ_BY_GROUP_REQ, 0x0000, |
| ATT_ECODE_UNSUPP_GRP_TYPE, pdu, len); |
| |
| last_handle = end; |
| database = channel->server->database; |
| for (dl = database, groups = NULL, cur = NULL; dl; dl = dl->next) { |
| |
| a = dl->data; |
| |
| if (a->handle < start) |
| continue; |
| |
| if (a->handle >= end) |
| break; |
| |
| /* The old group ends when a new one starts */ |
| if (old && (bt_uuid_cmp(&a->uuid, &prim_uuid) == 0 || |
| bt_uuid_cmp(&a->uuid, &snd_uuid) == 0)) { |
| old->end = last_handle; |
| old = NULL; |
| } |
| |
| if (bt_uuid_cmp(&a->uuid, uuid) != 0) { |
| /* Still inside a service, update its last handle */ |
| if (old) |
| last_handle = a->handle; |
| continue; |
| } |
| |
| if (last_size && (last_size != a->len)) |
| break; |
| |
| status = att_check_reqs(channel, ATT_OP_READ_BY_GROUP_REQ, |
| a->read_req); |
| |
| if (status == 0x00 && a->read_cb) |
| status = a->read_cb(a, channel->device, |
| a->cb_user_data); |
| |
| if (status) { |
| g_slist_free_full(groups, g_free); |
| return enc_error_resp(ATT_OP_READ_BY_GROUP_REQ, |
| a->handle, status, pdu, len); |
| } |
| |
| cur = g_new0(struct group_elem, 1); |
| cur->handle = a->handle; |
| cur->data = a->data; |
| cur->len = a->len; |
| |
| /* Attribute Grouping Type found */ |
| groups = g_slist_append(groups, cur); |
| |
| last_size = a->len; |
| old = cur; |
| last_handle = cur->handle; |
| } |
| |
| if (groups == NULL) |
| return enc_error_resp(ATT_OP_READ_BY_GROUP_REQ, start, |
| ATT_ECODE_ATTR_NOT_FOUND, pdu, len); |
| |
| if (dl == NULL) |
| cur->end = a->handle; |
| else |
| cur->end = last_handle; |
| |
| length = g_slist_length(groups); |
| |
| adl = att_data_list_alloc(length, last_size + 4); |
| if (adl == NULL) { |
| g_slist_free_full(groups, g_free); |
| return enc_error_resp(ATT_OP_READ_BY_GROUP_REQ, start, |
| ATT_ECODE_UNLIKELY, pdu, len); |
| } |
| |
| for (i = 0, l = groups; l; l = l->next, i++) { |
| uint8_t *value; |
| |
| cur = l->data; |
| |
| value = (void *) adl->data[i]; |
| |
| put_le16(cur->handle, value); |
| put_le16(cur->end, &value[2]); |
| /* Attribute Value */ |
| memcpy(&value[4], cur->data, cur->len); |
| } |
| |
| length = enc_read_by_grp_resp(adl, pdu, len); |
| |
| att_data_list_free(adl); |
| g_slist_free_full(groups, g_free); |
| |
| return length; |
| } |
| |
| static uint16_t read_by_type(struct gatt_channel *channel, uint16_t start, |
| uint16_t end, bt_uuid_t *uuid, |
| uint8_t *pdu, size_t len) |
| { |
| struct att_data_list *adl; |
| GSList *l, *types; |
| GList *dl, *database; |
| struct attribute *a; |
| uint16_t num, length; |
| uint8_t status; |
| int i; |
| |
| if (start > end || start == 0x0000) |
| return enc_error_resp(ATT_OP_READ_BY_TYPE_REQ, start, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| database = channel->server->database; |
| for (dl = database, length = 0, types = NULL; dl; dl = dl->next) { |
| |
| a = dl->data; |
| |
| if (a->handle < start) |
| continue; |
| |
| if (a->handle > end) |
| break; |
| |
| if (bt_uuid_cmp(&a->uuid, uuid) != 0) |
| continue; |
| |
| status = att_check_reqs(channel, ATT_OP_READ_BY_TYPE_REQ, |
| a->read_req); |
| |
| if (status == 0x00 && a->read_cb) |
| status = a->read_cb(a, channel->device, |
| a->cb_user_data); |
| |
| if (status) { |
| g_slist_free(types); |
| return enc_error_resp(ATT_OP_READ_BY_TYPE_REQ, |
| a->handle, status, pdu, len); |
| } |
| |
| /* All elements must have the same length */ |
| if (length == 0) |
| length = a->len; |
| else if (a->len != length) |
| break; |
| |
| types = g_slist_append(types, a); |
| } |
| |
| if (types == NULL) |
| return enc_error_resp(ATT_OP_READ_BY_TYPE_REQ, start, |
| ATT_ECODE_ATTR_NOT_FOUND, pdu, len); |
| |
| num = g_slist_length(types); |
| |
| /* Handle length plus attribute value length */ |
| length += 2; |
| |
| adl = att_data_list_alloc(num, length); |
| if (adl == NULL) { |
| g_slist_free(types); |
| return enc_error_resp(ATT_OP_READ_BY_TYPE_REQ, start, |
| ATT_ECODE_UNLIKELY, pdu, len); |
| } |
| |
| for (i = 0, l = types; l; i++, l = l->next) { |
| uint8_t *value; |
| |
| a = l->data; |
| |
| value = (void *) adl->data[i]; |
| |
| put_le16(a->handle, value); |
| |
| /* Attribute Value */ |
| memcpy(&value[2], a->data, a->len); |
| } |
| |
| length = enc_read_by_type_resp(adl, pdu, len); |
| |
| att_data_list_free(adl); |
| g_slist_free(types); |
| |
| return length; |
| } |
| |
| static uint16_t find_info(struct gatt_channel *channel, uint16_t start, |
| uint16_t end, uint8_t *pdu, size_t len) |
| { |
| struct attribute *a; |
| struct att_data_list *adl; |
| GSList *l, *info; |
| GList *dl, *database; |
| uint8_t format, last_type = BT_UUID_UNSPEC; |
| uint16_t length, num; |
| int i; |
| |
| if (start > end || start == 0x0000) |
| return enc_error_resp(ATT_OP_FIND_INFO_REQ, start, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| database = channel->server->database; |
| for (dl = database, info = NULL, num = 0; dl; dl = dl->next) { |
| a = dl->data; |
| |
| if (a->handle < start) |
| continue; |
| |
| if (a->handle > end) |
| break; |
| |
| if (last_type == BT_UUID_UNSPEC) |
| last_type = a->uuid.type; |
| |
| if (a->uuid.type != last_type) |
| break; |
| |
| info = g_slist_append(info, a); |
| num++; |
| |
| last_type = a->uuid.type; |
| } |
| |
| if (info == NULL) |
| return enc_error_resp(ATT_OP_FIND_INFO_REQ, start, |
| ATT_ECODE_ATTR_NOT_FOUND, pdu, len); |
| |
| if (last_type == BT_UUID16) { |
| length = 2; |
| format = 0x01; |
| } else if (last_type == BT_UUID128) { |
| length = 16; |
| format = 0x02; |
| } else { |
| g_slist_free(info); |
| return 0; |
| } |
| |
| adl = att_data_list_alloc(num, length + 2); |
| if (adl == NULL) { |
| g_slist_free(info); |
| return enc_error_resp(ATT_OP_FIND_INFO_REQ, start, |
| ATT_ECODE_UNLIKELY, pdu, len); |
| } |
| |
| for (i = 0, l = info; l; i++, l = l->next) { |
| uint8_t *value; |
| |
| a = l->data; |
| |
| value = (void *) adl->data[i]; |
| |
| put_le16(a->handle, value); |
| |
| /* Attribute Value */ |
| bt_uuid_to_le(&a->uuid, &value[2]); |
| } |
| |
| length = enc_find_info_resp(format, adl, pdu, len); |
| |
| att_data_list_free(adl); |
| g_slist_free(info); |
| |
| return length; |
| } |
| |
| static uint16_t find_by_type(struct gatt_channel *channel, uint16_t start, |
| uint16_t end, bt_uuid_t *uuid, |
| const uint8_t *value, size_t vlen, |
| uint8_t *opdu, size_t mtu) |
| { |
| struct attribute *a; |
| struct att_range *range; |
| GSList *matches; |
| GList *dl, *database; |
| uint16_t len; |
| |
| if (start > end || start == 0x0000) |
| return enc_error_resp(ATT_OP_FIND_BY_TYPE_REQ, start, |
| ATT_ECODE_INVALID_HANDLE, opdu, mtu); |
| |
| /* Searching first requested handle number */ |
| database = channel->server->database; |
| for (dl = database, matches = NULL, range = NULL; dl; dl = dl->next) { |
| a = dl->data; |
| |
| if (a->handle < start) |
| continue; |
| |
| if (a->handle > end) |
| break; |
| |
| /* Primary service? Attribute value matches? */ |
| if ((bt_uuid_cmp(&a->uuid, uuid) == 0) && (a->len == vlen) && |
| (memcmp(a->data, value, vlen) == 0)) { |
| |
| range = g_new0(struct att_range, 1); |
| range->start = a->handle; |
| /* It is allowed to have end group handle the same as |
| * start handle, for groups with only one attribute. */ |
| range->end = a->handle; |
| |
| matches = g_slist_append(matches, range); |
| } else if (range) { |
| /* Update the last found handle or reset the pointer |
| * to track that a new group started: Primary or |
| * Secondary service. */ |
| if (bt_uuid_cmp(&a->uuid, &prim_uuid) == 0 || |
| bt_uuid_cmp(&a->uuid, &snd_uuid) == 0) |
| range = NULL; |
| else |
| range->end = a->handle; |
| } |
| } |
| |
| if (matches == NULL) |
| return enc_error_resp(ATT_OP_FIND_BY_TYPE_REQ, start, |
| ATT_ECODE_ATTR_NOT_FOUND, opdu, mtu); |
| |
| len = enc_find_by_type_resp(matches, opdu, mtu); |
| |
| g_slist_free_full(matches, g_free); |
| |
| return len; |
| } |
| |
| static int read_device_ccc(struct btd_device *device, uint16_t handle, |
| uint16_t *value) |
| { |
| char *filename; |
| GKeyFile *key_file; |
| char group[6]; |
| char *str; |
| unsigned int config; |
| int err = 0; |
| |
| filename = btd_device_get_storage_path(device, "ccc"); |
| if (!filename) { |
| warn("Unable to get ccc storage path for device"); |
| return -ENOENT; |
| } |
| |
| key_file = g_key_file_new(); |
| g_key_file_load_from_file(key_file, filename, 0, NULL); |
| |
| sprintf(group, "%hu", handle); |
| |
| str = g_key_file_get_string(key_file, group, "Value", NULL); |
| if (!str || sscanf(str, "%04X", &config) != 1) |
| err = -ENOENT; |
| else |
| *value = config; |
| |
| g_free(str); |
| g_free(filename); |
| g_key_file_free(key_file); |
| |
| return err; |
| } |
| |
| static uint16_t read_value(struct gatt_channel *channel, uint16_t handle, |
| uint8_t *pdu, size_t len) |
| { |
| struct attribute *a; |
| uint8_t status; |
| GList *l; |
| uint16_t cccval; |
| guint h = handle; |
| |
| l = g_list_find_custom(channel->server->database, |
| GUINT_TO_POINTER(h), handle_cmp); |
| if (!l) |
| return enc_error_resp(ATT_OP_READ_REQ, handle, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| a = l->data; |
| |
| if (bt_uuid_cmp(&ccc_uuid, &a->uuid) == 0 && |
| read_device_ccc(channel->device, handle, &cccval) == 0) { |
| uint8_t config[2]; |
| |
| put_le16(cccval, config); |
| return enc_read_resp(config, sizeof(config), pdu, len); |
| } |
| |
| status = att_check_reqs(channel, ATT_OP_READ_REQ, a->read_req); |
| |
| if (status == 0x00 && a->read_cb) |
| status = a->read_cb(a, channel->device, a->cb_user_data); |
| |
| if (status) |
| return enc_error_resp(ATT_OP_READ_REQ, handle, status, pdu, |
| len); |
| |
| return enc_read_resp(a->data, a->len, pdu, len); |
| } |
| |
| static uint16_t read_blob(struct gatt_channel *channel, uint16_t handle, |
| uint16_t offset, uint8_t *pdu, size_t len) |
| { |
| struct attribute *a; |
| uint8_t status; |
| GList *l; |
| uint16_t cccval; |
| guint h = handle; |
| |
| l = g_list_find_custom(channel->server->database, |
| GUINT_TO_POINTER(h), handle_cmp); |
| if (!l) |
| return enc_error_resp(ATT_OP_READ_BLOB_REQ, handle, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| a = l->data; |
| |
| if (a->len < offset) |
| return enc_error_resp(ATT_OP_READ_BLOB_REQ, handle, |
| ATT_ECODE_INVALID_OFFSET, pdu, len); |
| |
| if (bt_uuid_cmp(&ccc_uuid, &a->uuid) == 0 && |
| read_device_ccc(channel->device, handle, &cccval) == 0) { |
| uint8_t config[2]; |
| |
| put_le16(cccval, config); |
| return enc_read_blob_resp(config, sizeof(config), offset, |
| pdu, len); |
| } |
| |
| status = att_check_reqs(channel, ATT_OP_READ_BLOB_REQ, a->read_req); |
| |
| if (status == 0x00 && a->read_cb) |
| status = a->read_cb(a, channel->device, a->cb_user_data); |
| |
| if (status) |
| return enc_error_resp(ATT_OP_READ_BLOB_REQ, handle, status, |
| pdu, len); |
| |
| return enc_read_blob_resp(a->data, a->len, offset, pdu, len); |
| } |
| |
| static uint16_t write_value(struct gatt_channel *channel, uint16_t handle, |
| const uint8_t *value, size_t vlen, |
| uint8_t *pdu, size_t len) |
| { |
| struct attribute *a; |
| uint8_t status; |
| GList *l; |
| guint h = handle; |
| |
| l = g_list_find_custom(channel->server->database, |
| GUINT_TO_POINTER(h), handle_cmp); |
| if (!l) |
| return enc_error_resp(ATT_OP_WRITE_REQ, handle, |
| ATT_ECODE_INVALID_HANDLE, pdu, len); |
| |
| a = l->data; |
| |
| status = att_check_reqs(channel, ATT_OP_WRITE_REQ, a->write_req); |
| if (status) |
| return enc_error_resp(ATT_OP_WRITE_REQ, handle, status, pdu, |
| len); |
| |
| if (bt_uuid_cmp(&ccc_uuid, &a->uuid) != 0) { |
| |
| attrib_db_update(channel->server->adapter, handle, NULL, |
| value, vlen, NULL); |
| |
| if (a->write_cb) { |
| status = a->write_cb(a, channel->device, |
| a->cb_user_data); |
| if (status) |
| return enc_error_resp(ATT_OP_WRITE_REQ, handle, |
| status, pdu, len); |
| } |
| } else { |
| uint16_t cccval = get_le16(value); |
| char *filename; |
| GKeyFile *key_file; |
| char group[6], value[5]; |
| char *data; |
| gsize length = 0; |
| |
| filename = btd_device_get_storage_path(channel->device, "ccc"); |
| if (!filename) { |
| warn("Unable to get ccc storage path for device"); |
| return enc_error_resp(ATT_OP_WRITE_REQ, handle, |
| ATT_ECODE_WRITE_NOT_PERM, |
| pdu, len); |
| } |
| |
| key_file = g_key_file_new(); |
| g_key_file_load_from_file(key_file, filename, 0, NULL); |
| |
| sprintf(group, "%hu", handle); |
| sprintf(value, "%hX", cccval); |
| g_key_file_set_string(key_file, group, "Value", value); |
| |
| data = g_key_file_to_data(key_file, &length, NULL); |
| if (length > 0) { |
| create_file(filename, S_IRUSR | S_IWUSR); |
| g_file_set_contents(filename, data, length, NULL); |
| } |
| |
| g_free(data); |
| g_free(filename); |
| g_key_file_free(key_file); |
| } |
| |
| return enc_write_resp(pdu); |
| } |
| |
| static uint16_t mtu_exchange(struct gatt_channel *channel, uint16_t mtu, |
| uint8_t *pdu, size_t len) |
| { |
| GError *gerr = NULL; |
| GIOChannel *io; |
| uint16_t imtu; |
| |
| if (mtu < ATT_DEFAULT_LE_MTU) |
| return enc_error_resp(ATT_OP_MTU_REQ, 0, |
| ATT_ECODE_REQ_NOT_SUPP, pdu, len); |
| |
| io = g_attrib_get_channel(channel->attrib); |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_IMTU, &imtu, BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| return enc_error_resp(ATT_OP_MTU_REQ, 0, ATT_ECODE_UNLIKELY, |
| pdu, len); |
| } |
| |
| channel->mtu = MIN(mtu, imtu); |
| g_attrib_set_mtu(channel->attrib, channel->mtu); |
| |
| return enc_mtu_resp(imtu, pdu, len); |
| } |
| |
| static void channel_remove(struct gatt_channel *channel) |
| { |
| channel->server->clients = g_slist_remove(channel->server->clients, |
| channel); |
| channel_free(channel); |
| } |
| |
| static gboolean channel_watch_cb(GIOChannel *io, GIOCondition cond, |
| gpointer user_data) |
| { |
| channel_remove(user_data); |
| |
| return FALSE; |
| } |
| |
| static void channel_handler(const uint8_t *ipdu, uint16_t len, |
| gpointer user_data) |
| { |
| struct gatt_channel *channel = user_data; |
| uint8_t opdu[channel->mtu]; |
| uint16_t length, start, end, mtu, offset; |
| bt_uuid_t uuid; |
| uint8_t status = 0; |
| size_t vlen; |
| uint8_t *value = g_attrib_get_buffer(channel->attrib, &vlen); |
| |
| // suppress very common debug message |
| // DBG("op 0x%02x", ipdu[0]); |
| |
| if (len > vlen) { |
| error("Too much data on ATT socket"); |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| switch (ipdu[0]) { |
| case ATT_OP_READ_BY_GROUP_REQ: |
| length = dec_read_by_grp_req(ipdu, len, &start, &end, &uuid); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = read_by_group(channel, start, end, &uuid, opdu, |
| channel->mtu); |
| break; |
| case ATT_OP_READ_BY_TYPE_REQ: |
| length = dec_read_by_type_req(ipdu, len, &start, &end, &uuid); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = read_by_type(channel, start, end, &uuid, opdu, |
| channel->mtu); |
| break; |
| case ATT_OP_READ_REQ: |
| length = dec_read_req(ipdu, len, &start); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = read_value(channel, start, opdu, channel->mtu); |
| break; |
| case ATT_OP_READ_BLOB_REQ: |
| length = dec_read_blob_req(ipdu, len, &start, &offset); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = read_blob(channel, start, offset, opdu, channel->mtu); |
| break; |
| case ATT_OP_MTU_REQ: |
| if (!channel->le) { |
| status = ATT_ECODE_REQ_NOT_SUPP; |
| goto done; |
| } |
| |
| length = dec_mtu_req(ipdu, len, &mtu); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = mtu_exchange(channel, mtu, opdu, channel->mtu); |
| break; |
| case ATT_OP_FIND_INFO_REQ: |
| length = dec_find_info_req(ipdu, len, &start, &end); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = find_info(channel, start, end, opdu, channel->mtu); |
| break; |
| case ATT_OP_WRITE_REQ: |
| length = dec_write_req(ipdu, len, &start, value, &vlen); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = write_value(channel, start, value, vlen, opdu, |
| channel->mtu); |
| break; |
| case ATT_OP_WRITE_CMD: |
| length = dec_write_cmd(ipdu, len, &start, value, &vlen); |
| if (length > 0) |
| write_value(channel, start, value, vlen, opdu, |
| channel->mtu); |
| return; |
| case ATT_OP_FIND_BY_TYPE_REQ: |
| length = dec_find_by_type_req(ipdu, len, &start, &end, |
| &uuid, value, &vlen); |
| if (length == 0) { |
| status = ATT_ECODE_INVALID_PDU; |
| goto done; |
| } |
| |
| length = find_by_type(channel, start, end, &uuid, value, vlen, |
| opdu, channel->mtu); |
| break; |
| case ATT_OP_HANDLE_CNF: |
| return; |
| case ATT_OP_HANDLE_IND: |
| case ATT_OP_HANDLE_NOTIFY: |
| /* The attribute client is already handling these */ |
| return; |
| case ATT_OP_READ_MULTI_REQ: |
| case ATT_OP_PREP_WRITE_REQ: |
| case ATT_OP_EXEC_WRITE_REQ: |
| default: |
| DBG("Unsupported request 0x%02x", ipdu[0]); |
| status = ATT_ECODE_REQ_NOT_SUPP; |
| goto done; |
| } |
| |
| if (length == 0) |
| status = ATT_ECODE_IO; |
| |
| done: |
| if (status) |
| length = enc_error_resp(ipdu[0], 0x0000, status, opdu, |
| channel->mtu); |
| |
| g_attrib_send(channel->attrib, 0, opdu, length, NULL, NULL, NULL); |
| } |
| |
| GAttrib *attrib_from_device(struct btd_device *device) |
| { |
| struct btd_adapter *adapter = device_get_adapter(device); |
| struct gatt_server *server; |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (!l) |
| return NULL; |
| |
| server = l->data; |
| |
| for (l = server->clients; l; l = l->next) { |
| struct gatt_channel *channel = l->data; |
| |
| if (channel->device == device) |
| return g_attrib_ref(channel->attrib); |
| } |
| |
| return NULL; |
| } |
| |
| guint attrib_channel_attach(GAttrib *attrib) |
| { |
| struct gatt_server *server; |
| struct btd_device *device; |
| struct gatt_channel *channel; |
| bdaddr_t src, dst; |
| GIOChannel *io; |
| GError *gerr = NULL; |
| uint8_t bdaddr_type; |
| uint16_t cid; |
| guint mtu = 0; |
| |
| io = g_attrib_get_channel(attrib); |
| |
| bt_io_get(io, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_DEST_TYPE, &bdaddr_type, |
| BT_IO_OPT_CID, &cid, |
| BT_IO_OPT_IMTU, &mtu, |
| BT_IO_OPT_INVALID); |
| if (gerr) { |
| error("bt_io_get: %s", gerr->message); |
| g_error_free(gerr); |
| return 0; |
| } |
| |
| server = find_gatt_server(&src); |
| if (server == NULL) |
| return 0; |
| |
| channel = g_new0(struct gatt_channel, 1); |
| channel->server = server; |
| |
| device = btd_adapter_find_device(server->adapter, &dst, bdaddr_type); |
| if (device == NULL) { |
| error("Device object not found for attrib server"); |
| g_free(channel); |
| return 0; |
| } |
| |
| if (!device_is_bonded(device, bdaddr_type)) { |
| char *filename; |
| |
| filename = btd_device_get_storage_path(device, "ccc"); |
| if (filename) { |
| unlink(filename); |
| g_free(filename); |
| } |
| } |
| |
| if (cid != ATT_CID) { |
| channel->le = FALSE; |
| channel->mtu = mtu; |
| } else { |
| channel->le = TRUE; |
| channel->mtu = ATT_DEFAULT_LE_MTU; |
| } |
| |
| channel->attrib = g_attrib_ref(attrib); |
| channel->id = g_attrib_register(channel->attrib, GATTRIB_ALL_REQS, |
| GATTRIB_ALL_HANDLES, channel_handler, channel, NULL); |
| |
| channel->cleanup_id = g_io_add_watch(io, G_IO_HUP, channel_watch_cb, |
| channel); |
| |
| channel->device = btd_device_ref(device); |
| |
| server->clients = g_slist_append(server->clients, channel); |
| |
| return channel->id; |
| } |
| |
| static struct gatt_channel *find_channel(guint id) |
| { |
| GSList *l; |
| |
| for (l = servers; l; l = g_slist_next(l)) { |
| struct gatt_server *server = l->data; |
| GSList *c; |
| |
| for (c = server->clients; c; c = g_slist_next(c)) { |
| struct gatt_channel *channel = c->data; |
| |
| if (channel->id == id) |
| return channel; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| gboolean attrib_channel_detach(GAttrib *attrib, guint id) |
| { |
| struct gatt_channel *channel; |
| |
| channel = find_channel(id); |
| if (channel == NULL) |
| return FALSE; |
| |
| g_attrib_unregister(channel->attrib, channel->id); |
| channel_remove(channel); |
| |
| return TRUE; |
| } |
| |
| static void connect_event(GIOChannel *io, GError *gerr, void *user_data) |
| { |
| struct btd_adapter *adapter; |
| struct btd_device *device; |
| uint8_t dst_type; |
| bdaddr_t src, dst; |
| |
| DBG(""); |
| |
| 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; |
| } |
| |
| 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 gboolean register_core_services(struct gatt_server *server) |
| { |
| uint8_t atval[256]; |
| bt_uuid_t uuid; |
| uint16_t appearance = 0x0000; |
| |
| /* GAP service: primary service definition */ |
| bt_uuid16_create(&uuid, GATT_PRIM_SVC_UUID); |
| put_le16(GENERIC_ACCESS_PROFILE_ID, &atval[0]); |
| attrib_db_add_new(server, 0x0001, &uuid, ATT_NONE, ATT_NOT_PERMITTED, |
| atval, 2); |
| |
| /* GAP service: device name characteristic */ |
| server->name_handle = 0x0006; |
| bt_uuid16_create(&uuid, GATT_CHARAC_UUID); |
| atval[0] = GATT_CHR_PROP_READ; |
| put_le16(server->name_handle, &atval[1]); |
| put_le16(GATT_CHARAC_DEVICE_NAME, &atval[3]); |
| attrib_db_add_new(server, 0x0004, &uuid, ATT_NONE, ATT_NOT_PERMITTED, |
| atval, 5); |
| |
| /* GAP service: device name attribute */ |
| const char* name = btd_adapter_get_name(server->adapter); |
| bt_uuid16_create(&uuid, GATT_CHARAC_DEVICE_NAME); |
| attrib_db_add_new(server, server->name_handle, &uuid, ATT_NONE, |
| ATT_NOT_PERMITTED, name, |
| strlen(name)); |
| |
| /* GAP service: device appearance characteristic */ |
| server->appearance_handle = 0x0008; |
| bt_uuid16_create(&uuid, GATT_CHARAC_UUID); |
| atval[0] = GATT_CHR_PROP_READ; |
| put_le16(server->appearance_handle, &atval[1]); |
| put_le16(GATT_CHARAC_APPEARANCE, &atval[3]); |
| attrib_db_add_new(server, 0x0007, &uuid, ATT_NONE, ATT_NOT_PERMITTED, |
| atval, 5); |
| |
| /* GAP service: device appearance attribute */ |
| bt_uuid16_create(&uuid, GATT_CHARAC_APPEARANCE); |
| put_le16(appearance, &atval[0]); |
| attrib_db_add_new(server, server->appearance_handle, &uuid, ATT_NONE, |
| ATT_NOT_PERMITTED, atval, 2); |
| server->gap_sdp_handle = attrib_create_sdp_new(server, 0x0001, |
| "Generic Access Profile"); |
| if (server->gap_sdp_handle == 0) { |
| error("Failed to register GAP service record"); |
| return FALSE; |
| } |
| |
| /* GATT service: primary service definition */ |
| bt_uuid16_create(&uuid, GATT_PRIM_SVC_UUID); |
| put_le16(GENERIC_ATTRIB_PROFILE_ID, &atval[0]); |
| attrib_db_add_new(server, 0x0010, &uuid, ATT_NONE, ATT_NOT_PERMITTED, |
| atval, 2); |
| |
| server->gatt_sdp_handle = attrib_create_sdp_new(server, 0x0010, |
| "Generic Attribute Profile"); |
| if (server->gatt_sdp_handle == 0) { |
| error("Failed to register GATT service record"); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| int btd_adapter_gatt_server_start(struct btd_adapter *adapter) |
| { |
| struct gatt_server *server; |
| GError *gerr = NULL; |
| const bdaddr_t *addr; |
| |
| DBG("Start GATT server in hci%d", btd_adapter_get_index(adapter)); |
| |
| server = g_new0(struct gatt_server, 1); |
| server->adapter = btd_adapter_ref(adapter); |
| |
| addr = btd_adapter_get_address(server->adapter); |
| |
| /* BR/EDR socket */ |
| server->l2cap_io = bt_io_listen(connect_event, NULL, NULL, NULL, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, addr, |
| BT_IO_OPT_PSM, ATT_PSM, |
| BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_LOW, |
| BT_IO_OPT_INVALID); |
| |
| if (server->l2cap_io == NULL) { |
| error("%s", gerr->message); |
| g_error_free(gerr); |
| gatt_server_free(server); |
| return -1; |
| } |
| |
| if (!register_core_services(server)) { |
| gatt_server_free(server); |
| return -1; |
| } |
| |
| /* LE socket */ |
| server->le_io = bt_io_listen(connect_event, NULL, |
| &server->le_io, NULL, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, addr, |
| BT_IO_OPT_SOURCE_TYPE, BDADDR_LE_PUBLIC, |
| BT_IO_OPT_CID, ATT_CID, |
| BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_LOW, |
| BT_IO_OPT_INVALID); |
| |
| if (server->le_io == NULL) { |
| error("%s", gerr->message); |
| g_error_free(gerr); |
| /* Doesn't have LE support, continue */ |
| } |
| |
| servers = g_slist_prepend(servers, server); |
| return 0; |
| } |
| |
| void btd_adapter_gatt_server_stop(struct btd_adapter *adapter) |
| { |
| struct gatt_server *server; |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return; |
| |
| DBG("Stop GATT server in hci%d", btd_adapter_get_index(adapter)); |
| |
| server = l->data; |
| servers = g_slist_remove(servers, server); |
| gatt_server_free(server); |
| } |
| |
| uint32_t attrib_create_sdp(struct btd_adapter *adapter, uint16_t handle, |
| const char *name) |
| { |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return 0; |
| |
| return attrib_create_sdp_new(l->data, handle, name); |
| } |
| |
| void attrib_free_sdp(struct btd_adapter *adapter, uint32_t sdp_handle) |
| { |
| adapter_service_remove(adapter, sdp_handle); |
| } |
| |
| static uint16_t find_uuid16_avail(struct btd_adapter *adapter, uint16_t nitems) |
| { |
| struct gatt_server *server; |
| uint16_t handle; |
| GSList *l; |
| GList *dl; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return 0; |
| |
| server = l->data; |
| if (server->database == NULL) |
| return 0x0001; |
| |
| for (dl = server->database, handle = 0x0001; dl; dl = dl->next) { |
| struct attribute *a = dl->data; |
| |
| if ((bt_uuid_cmp(&a->uuid, &prim_uuid) == 0 || |
| bt_uuid_cmp(&a->uuid, &snd_uuid) == 0) && |
| a->handle - handle >= nitems) |
| /* Note: the range above excludes the current handle */ |
| return handle; |
| |
| if (a->len == 16 && (bt_uuid_cmp(&a->uuid, &prim_uuid) == 0 || |
| bt_uuid_cmp(&a->uuid, &snd_uuid) == 0)) { |
| /* 128 bit UUID service definition */ |
| return 0; |
| } |
| |
| if (a->handle == 0xffff) |
| return 0; |
| |
| handle = a->handle + 1; |
| } |
| |
| if (0xffff - handle + 1 >= nitems) |
| return handle; |
| |
| return 0; |
| } |
| |
| static uint16_t find_uuid128_avail(struct btd_adapter *adapter, uint16_t nitems) |
| { |
| uint16_t handle = 0, end = 0xffff; |
| struct gatt_server *server; |
| GList *dl; |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return 0; |
| |
| server = l->data; |
| if (server->database == NULL) |
| return 0xffff - nitems + 1; |
| |
| for (dl = g_list_last(server->database); dl; dl = dl->prev) { |
| struct attribute *a = dl->data; |
| |
| if (handle == 0) |
| handle = a->handle; |
| |
| if (bt_uuid_cmp(&a->uuid, &prim_uuid) != 0 && |
| bt_uuid_cmp(&a->uuid, &snd_uuid) != 0) |
| continue; |
| |
| if (end - handle >= nitems) |
| return end - nitems + 1; |
| |
| if (a->len == 2) { |
| /* 16 bit UUID service definition */ |
| return 0; |
| } |
| |
| if (a->handle == 0x0001) |
| return 0; |
| |
| end = a->handle - 1; |
| handle = 0; |
| } |
| |
| if (end - 0x0001 >= nitems) |
| return end - nitems + 1; |
| |
| return 0; |
| } |
| |
| uint16_t attrib_db_find_avail(struct btd_adapter *adapter, bt_uuid_t *svc_uuid, |
| uint16_t nitems) |
| { |
| btd_assert(nitems > 0); |
| |
| if (svc_uuid->type == BT_UUID16) |
| return find_uuid16_avail(adapter, nitems); |
| else if (svc_uuid->type == BT_UUID128) |
| return find_uuid128_avail(adapter, nitems); |
| else { |
| char uuidstr[MAX_LEN_UUID_STR]; |
| |
| bt_uuid_to_string(svc_uuid, uuidstr, MAX_LEN_UUID_STR); |
| error("Service uuid: %s is neither a 16-bit nor a 128-bit uuid", |
| uuidstr); |
| return 0; |
| } |
| } |
| |
| struct attribute *attrib_db_add(struct btd_adapter *adapter, uint16_t handle, |
| bt_uuid_t *uuid, int read_req, |
| int write_req, const uint8_t *value, |
| size_t len) |
| { |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return NULL; |
| |
| return attrib_db_add_new(l->data, handle, uuid, read_req, write_req, |
| value, len); |
| } |
| |
| int attrib_db_update(struct btd_adapter *adapter, uint16_t handle, |
| bt_uuid_t *uuid, const uint8_t *value, |
| size_t len, struct attribute **attr) |
| { |
| struct gatt_server *server; |
| struct attribute *a; |
| GSList *l; |
| GList *dl; |
| guint h = handle; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return -ENOENT; |
| |
| server = l->data; |
| |
| DBG("handle=0x%04x", handle); |
| |
| dl = g_list_find_custom(server->database, GUINT_TO_POINTER(h), |
| handle_cmp); |
| if (dl == NULL) |
| return -ENOENT; |
| |
| a = dl->data; |
| |
| a->data = g_try_realloc(a->data, len); |
| if (len && a->data == NULL) |
| return -ENOMEM; |
| |
| a->len = len; |
| memcpy(a->data, value, len); |
| |
| if (uuid != NULL) |
| a->uuid = *uuid; |
| |
| if (attr) |
| *attr = a; |
| |
| return 0; |
| } |
| |
| int attrib_db_del(struct btd_adapter *adapter, uint16_t handle) |
| { |
| struct gatt_server *server; |
| struct attribute *a; |
| GSList *l; |
| GList *dl; |
| guint h = handle; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return -ENOENT; |
| |
| server = l->data; |
| |
| DBG("handle=0x%04x", handle); |
| |
| dl = g_list_find_custom(server->database, GUINT_TO_POINTER(h), |
| handle_cmp); |
| if (dl == NULL) |
| return -ENOENT; |
| |
| a = dl->data; |
| server->database = g_list_remove(server->database, a); |
| g_free(a->data); |
| g_free(a); |
| |
| return 0; |
| } |
| |
| int attrib_gap_set(struct btd_adapter *adapter, uint16_t uuid, |
| const uint8_t *value, size_t len) |
| { |
| struct gatt_server *server; |
| uint16_t handle; |
| GSList *l; |
| |
| l = g_slist_find_custom(servers, adapter, adapter_cmp); |
| if (l == NULL) |
| return -ENOENT; |
| |
| server = l->data; |
| |
| /* FIXME: Missing Privacy and Reconnection Address */ |
| |
| switch (uuid) { |
| case GATT_CHARAC_DEVICE_NAME: |
| handle = server->name_handle; |
| break; |
| case GATT_CHARAC_APPEARANCE: |
| handle = server->appearance_handle; |
| break; |
| default: |
| return -ENOSYS; |
| } |
| |
| return attrib_db_update(adapter, handle, NULL, value, len, NULL); |
| } |