| // SPDX-License-Identifier: LGPL-2.1-or-later |
| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2014 Google Inc. |
| * |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <errno.h> |
| |
| #include "src/shared/io.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/util.h" |
| #include "src/shared/timeout.h" |
| #include "bluetooth/bluetooth.h" |
| #include "bluetooth/l2cap.h" |
| #include "bluetooth/uuid.h" |
| #include "src/shared/att.h" |
| #include "src/shared/crypto.h" |
| |
| #define ATT_MIN_PDU_LEN 1 /* At least 1 byte for the opcode. */ |
| #define ATT_OP_CMD_MASK 0x40 |
| #define ATT_OP_SIGNED_MASK 0x80 |
| #define ATT_TIMEOUT_INTERVAL 30000 /* 30000 ms */ |
| |
| /* Length of signature in write signed packet */ |
| #define BT_ATT_SIGNATURE_LEN 12 |
| |
| struct att_send_op; |
| |
| struct bt_att_chan { |
| struct bt_att *att; |
| int fd; |
| struct io *io; |
| uint8_t type; |
| int sec_level; /* Only used for non-L2CAP */ |
| |
| struct queue *queue; /* Channel dedicated queue */ |
| |
| struct att_send_op *pending_req; |
| struct att_send_op *pending_ind; |
| struct att_send_op *pending_db_sync; |
| bool writer_active; |
| |
| bool in_req; /* There's a pending incoming request */ |
| |
| uint8_t *buf; |
| uint16_t mtu; |
| }; |
| |
| struct bt_att { |
| int ref_count; |
| bool close_on_unref; |
| struct queue *chans; |
| uint8_t enc_size; |
| uint16_t mtu; /* Biggest possible MTU */ |
| |
| struct queue *notify_list; /* List of registered callbacks */ |
| struct queue *disconn_list; /* List of disconnect handlers */ |
| struct queue *exchange_list; /* List of MTU changed handlers */ |
| |
| unsigned int next_send_id; /* IDs for "send" ops */ |
| unsigned int next_reg_id; /* IDs for registered callbacks */ |
| |
| struct queue *req_queue; /* Queued ATT protocol requests */ |
| struct queue *ind_queue; /* Queued ATT protocol indications */ |
| struct queue *write_queue; /* Queue of PDUs ready to send */ |
| bool in_disc; /* Cleanup queues on disconnect_cb */ |
| |
| bt_att_timeout_func_t timeout_callback; |
| bt_att_destroy_func_t timeout_destroy; |
| void *timeout_data; |
| |
| bt_att_db_sync_func_t db_sync_callback; |
| bt_att_destroy_func_t db_sync_destroy; |
| void *db_sync_data; |
| |
| uint8_t debug_level; |
| bt_att_debug_func_t debug_callback; |
| bt_att_destroy_func_t debug_destroy; |
| void *debug_data; |
| |
| struct bt_crypto *crypto; |
| |
| struct sign_info *local_sign; |
| struct sign_info *remote_sign; |
| }; |
| |
| struct sign_info { |
| uint8_t key[16]; |
| bt_att_counter_func_t counter; |
| void *user_data; |
| }; |
| |
| enum att_op_type { |
| ATT_OP_TYPE_REQ, |
| ATT_OP_TYPE_RSP, |
| ATT_OP_TYPE_CMD, |
| ATT_OP_TYPE_IND, |
| ATT_OP_TYPE_NFY, |
| ATT_OP_TYPE_CONF, |
| ATT_OP_TYPE_UNKNOWN, |
| }; |
| |
| static const struct { |
| uint8_t opcode; |
| enum att_op_type type; |
| } att_opcode_type_table[] = { |
| { BT_ATT_OP_ERROR_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_MTU_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_MTU_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_FIND_INFO_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_FIND_INFO_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_FIND_BY_TYPE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_FIND_BY_TYPE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_READ_BY_TYPE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_READ_BY_TYPE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_READ_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_READ_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_READ_BLOB_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_READ_BLOB_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_READ_MULT_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_READ_MULT_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_READ_BY_GRP_TYPE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_READ_BY_GRP_TYPE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_WRITE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_WRITE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_WRITE_CMD, ATT_OP_TYPE_CMD }, |
| { BT_ATT_OP_SIGNED_WRITE_CMD, ATT_OP_TYPE_CMD }, |
| { BT_ATT_OP_PREP_WRITE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_PREP_WRITE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_EXEC_WRITE_REQ, ATT_OP_TYPE_REQ }, |
| { BT_ATT_OP_EXEC_WRITE_RSP, ATT_OP_TYPE_RSP }, |
| { BT_ATT_OP_HANDLE_NFY, ATT_OP_TYPE_NFY }, |
| { BT_ATT_OP_HANDLE_NFY_MULT, ATT_OP_TYPE_NFY }, |
| { BT_ATT_OP_HANDLE_IND, ATT_OP_TYPE_IND }, |
| { BT_ATT_OP_HANDLE_CONF, ATT_OP_TYPE_CONF }, |
| { } |
| }; |
| |
| static enum att_op_type get_op_type(uint8_t opcode) |
| { |
| int i; |
| |
| for (i = 0; att_opcode_type_table[i].opcode; i++) { |
| if (att_opcode_type_table[i].opcode == opcode) |
| return att_opcode_type_table[i].type; |
| } |
| |
| if (opcode & ATT_OP_CMD_MASK) |
| return ATT_OP_TYPE_CMD; |
| |
| return ATT_OP_TYPE_UNKNOWN; |
| } |
| |
| static const struct { |
| uint8_t req_opcode; |
| uint8_t rsp_opcode; |
| } att_req_rsp_mapping_table[] = { |
| { BT_ATT_OP_MTU_REQ, BT_ATT_OP_MTU_RSP }, |
| { BT_ATT_OP_FIND_INFO_REQ, BT_ATT_OP_FIND_INFO_RSP}, |
| { BT_ATT_OP_FIND_BY_TYPE_REQ, BT_ATT_OP_FIND_BY_TYPE_RSP }, |
| { BT_ATT_OP_READ_BY_TYPE_REQ, BT_ATT_OP_READ_BY_TYPE_RSP }, |
| { BT_ATT_OP_READ_REQ, BT_ATT_OP_READ_RSP }, |
| { BT_ATT_OP_READ_BLOB_REQ, BT_ATT_OP_READ_BLOB_RSP }, |
| { BT_ATT_OP_READ_MULT_REQ, BT_ATT_OP_READ_MULT_RSP }, |
| { BT_ATT_OP_READ_BY_GRP_TYPE_REQ, BT_ATT_OP_READ_BY_GRP_TYPE_RSP }, |
| { BT_ATT_OP_WRITE_REQ, BT_ATT_OP_WRITE_RSP }, |
| { BT_ATT_OP_PREP_WRITE_REQ, BT_ATT_OP_PREP_WRITE_RSP }, |
| { BT_ATT_OP_EXEC_WRITE_REQ, BT_ATT_OP_EXEC_WRITE_RSP }, |
| { } |
| }; |
| |
| static uint8_t get_req_opcode(uint8_t rsp_opcode) |
| { |
| int i; |
| |
| for (i = 0; att_req_rsp_mapping_table[i].rsp_opcode; i++) { |
| if (att_req_rsp_mapping_table[i].rsp_opcode == rsp_opcode) |
| return att_req_rsp_mapping_table[i].req_opcode; |
| } |
| |
| return 0; |
| } |
| |
| struct att_send_op { |
| unsigned int id; |
| unsigned int timeout_id; |
| enum att_op_type type; |
| uint8_t opcode; |
| void *pdu; |
| uint16_t len; |
| bool retry; |
| bt_att_response_func_t callback; |
| bt_att_destroy_func_t destroy; |
| void *user_data; |
| }; |
| |
| static void destroy_att_send_op(void *data) |
| { |
| struct att_send_op *op = data; |
| |
| if (op->timeout_id) |
| timeout_remove(op->timeout_id); |
| |
| if (op->destroy) |
| op->destroy(op->user_data); |
| |
| free(op->pdu); |
| free(op); |
| } |
| |
| static void cancel_att_send_op(void *data) |
| { |
| struct att_send_op *op = data; |
| |
| if (op->destroy) |
| op->destroy(op->user_data); |
| |
| op->user_data = NULL; |
| op->callback = NULL; |
| op->destroy = NULL; |
| } |
| |
| struct att_notify { |
| unsigned int id; |
| uint16_t opcode; |
| bt_att_notify_func_t callback; |
| bt_att_destroy_func_t destroy; |
| void *user_data; |
| }; |
| |
| static void destroy_att_notify(void *data) |
| { |
| struct att_notify *notify = data; |
| |
| if (notify->destroy) |
| notify->destroy(notify->user_data); |
| |
| free(notify); |
| } |
| |
| static bool match_notify_id(const void *a, const void *b) |
| { |
| const struct att_notify *notify = a; |
| unsigned int id = PTR_TO_UINT(b); |
| |
| return notify->id == id; |
| } |
| |
| struct att_disconn { |
| unsigned int id; |
| bool removed; |
| bt_att_disconnect_func_t callback; |
| bt_att_destroy_func_t destroy; |
| void *user_data; |
| }; |
| |
| struct att_exchange { |
| unsigned int id; |
| bool removed; |
| bt_att_exchange_func_t callback; |
| bt_att_destroy_func_t destroy; |
| void *user_data; |
| }; |
| |
| static void destroy_att_disconn(void *data) |
| { |
| struct att_disconn *disconn = data; |
| |
| if (disconn->destroy) |
| disconn->destroy(disconn->user_data); |
| |
| free(disconn); |
| } |
| |
| static void destroy_att_exchange(void *data) |
| { |
| struct att_exchange *exchange = data; |
| |
| if (exchange->destroy) |
| exchange->destroy(exchange->user_data); |
| |
| free(exchange); |
| } |
| |
| static bool match_disconn_id(const void *a, const void *b) |
| { |
| const struct att_disconn *disconn = a; |
| unsigned int id = PTR_TO_UINT(b); |
| |
| return disconn->id == id; |
| } |
| |
| static void att_log(struct bt_att *att, uint8_t level, const char *format, |
| ...) |
| { |
| va_list va; |
| |
| if (att->debug_level < level) |
| return; |
| |
| va_start(va, format); |
| util_debug_va(att->debug_callback, att->debug_data, format, va); |
| va_end(va); |
| } |
| |
| #define DBG(_att, _format, _arg...) \ |
| att_log(_att, BT_ATT_DEBUG, "%s:%s() " _format, __FILE__, __func__,\ |
| ## _arg) |
| |
| #define VERBOSE(_att, _format, _arg...) \ |
| att_log(_att, BT_ATT_DEBUG_VERBOSE, "%s:%s() " _format, __FILE__, \ |
| __func__, ## _arg) |
| |
| static void att_hexdump(struct bt_att *att, char dir, const void *data, |
| size_t len) |
| { |
| if (att->debug_level < 2) |
| return; |
| |
| util_hexdump(dir, data, len, att->debug_callback, att->debug_data); |
| } |
| |
| static bool encode_pdu(struct bt_att *att, struct att_send_op *op, |
| const void *pdu, uint16_t length) |
| { |
| uint16_t pdu_len = 1; |
| struct sign_info *sign = att->local_sign; |
| uint32_t sign_cnt; |
| |
| if (sign && (op->opcode & ATT_OP_SIGNED_MASK)) |
| pdu_len += BT_ATT_SIGNATURE_LEN; |
| |
| if (length && pdu) |
| pdu_len += length; |
| |
| if (pdu_len > att->mtu) |
| return false; |
| |
| op->len = pdu_len; |
| op->pdu = malloc(op->len); |
| if (!op->pdu) |
| return false; |
| |
| ((uint8_t *) op->pdu)[0] = op->opcode; |
| if (pdu_len > 1) |
| memcpy(op->pdu + 1, pdu, length); |
| |
| if (!sign || !(op->opcode & ATT_OP_SIGNED_MASK) || !att->crypto) |
| return true; |
| |
| if (!sign->counter(&sign_cnt, sign->user_data)) |
| goto fail; |
| |
| if ((bt_crypto_sign_att(att->crypto, sign->key, op->pdu, 1 + length, |
| sign_cnt, &((uint8_t *) op->pdu)[1 + length]))) |
| return true; |
| |
| DBG(att, "ATT unable to generate signature"); |
| |
| fail: |
| free(op->pdu); |
| return false; |
| } |
| |
| static struct att_send_op *create_att_send_op(struct bt_att *att, |
| uint8_t opcode, |
| const void *pdu, |
| uint16_t length, |
| bt_att_response_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_send_op *op; |
| enum att_op_type type; |
| |
| if (length && !pdu) |
| return NULL; |
| |
| type = get_op_type(opcode); |
| if (type == ATT_OP_TYPE_UNKNOWN) |
| return NULL; |
| |
| /* If the opcode corresponds to an operation type that does not elicit a |
| * response from the remote end, then no callback should have been |
| * provided, since it will never be called. |
| */ |
| if (callback && type != ATT_OP_TYPE_REQ && type != ATT_OP_TYPE_IND) |
| return NULL; |
| |
| /* Similarly, if the operation does elicit a response then a callback |
| * must be provided. |
| */ |
| if (!callback && (type == ATT_OP_TYPE_REQ || type == ATT_OP_TYPE_IND)) |
| return NULL; |
| |
| op = new0(struct att_send_op, 1); |
| op->type = type; |
| op->opcode = opcode; |
| op->callback = callback; |
| op->destroy = destroy; |
| op->user_data = user_data; |
| |
| if (!encode_pdu(att, op, pdu, length)) { |
| free(op); |
| return NULL; |
| } |
| |
| return op; |
| } |
| |
| static struct att_send_op *pick_next_send_op(struct bt_att_chan *chan) |
| { |
| struct bt_att *att = chan->att; |
| struct att_send_op *op; |
| |
| /* Check if there is anything queued on the channel */ |
| op = queue_pop_head(chan->queue); |
| if (op) |
| return op; |
| |
| /* See if any operations are already in the write queue */ |
| op = queue_peek_head(att->write_queue); |
| if (op && op->len <= chan->mtu) |
| return queue_pop_head(att->write_queue); |
| |
| /* If there is no pending request, pick an operation from the |
| * request queue. |
| */ |
| if (!chan->pending_req) { |
| op = queue_peek_head(att->req_queue); |
| if (op && op->len <= chan->mtu) { |
| /* Don't send Exchange MTU over EATT */ |
| if (op->opcode == BT_ATT_OP_MTU_REQ && |
| chan->type == BT_ATT_EATT) |
| goto indicate; |
| |
| return queue_pop_head(att->req_queue); |
| } |
| } |
| |
| indicate: |
| /* There is either a request pending or no requests queued. If there is |
| * no pending indication, pick an operation from the indication queue. |
| */ |
| if (!chan->pending_ind) { |
| op = queue_peek_head(att->ind_queue); |
| if (op && op->len <= chan->mtu) |
| return queue_pop_head(att->ind_queue); |
| } |
| |
| return NULL; |
| } |
| |
| static void disc_att_send_op(void *data) |
| { |
| struct att_send_op *op = data; |
| |
| if (op->callback) |
| op->callback(BT_ATT_OP_ERROR_RSP, NULL, 0, op->user_data); |
| |
| destroy_att_send_op(op); |
| } |
| |
| struct timeout_data { |
| struct bt_att_chan *chan; |
| unsigned int id; |
| }; |
| |
| static bool timeout_cb(void *user_data) |
| { |
| struct timeout_data *timeout = user_data; |
| struct bt_att_chan *chan = timeout->chan; |
| struct bt_att *att = chan->att; |
| struct att_send_op *op = NULL; |
| |
| if (chan->pending_req && chan->pending_req->id == timeout->id) { |
| op = chan->pending_req; |
| chan->pending_req = NULL; |
| } else if (chan->pending_ind && chan->pending_ind->id == timeout->id) { |
| op = chan->pending_ind; |
| chan->pending_ind = NULL; |
| } |
| |
| if (!op) |
| return false; |
| |
| DBG(att, "(chan %p) Operation timed out: 0x%02x", chan, |
| op->opcode); |
| |
| if (att->timeout_callback) |
| att->timeout_callback(op->id, op->opcode, att->timeout_data); |
| |
| op->timeout_id = 0; |
| disc_att_send_op(op); |
| |
| /* |
| * Directly terminate the connection as required by the ATT protocol. |
| * This should trigger an io disconnect event which will clean up the |
| * io and notify the upper layer. |
| */ |
| io_shutdown(chan->io); |
| |
| return false; |
| } |
| |
| static void write_watch_destroy(void *user_data) |
| { |
| struct bt_att_chan *chan = user_data; |
| |
| chan->writer_active = false; |
| } |
| |
| static ssize_t bt_att_chan_write(struct bt_att_chan *chan, uint8_t opcode, |
| const void *pdu, uint16_t len) |
| { |
| struct bt_att *att = chan->att; |
| ssize_t ret; |
| struct iovec iov; |
| |
| iov.iov_base = (void *) pdu; |
| iov.iov_len = len; |
| |
| VERBOSE(att, "(chan %p) ATT op 0x%02x", chan, opcode); |
| |
| ret = io_send(chan->io, &iov, 1); |
| if (ret < 0) { |
| DBG(att, "(chan %p) write failed: %s", chan, |
| strerror(-ret)); |
| return ret; |
| } |
| |
| if (att->debug_level) |
| util_hexdump('<', pdu, ret, att->debug_callback, |
| att->debug_data); |
| |
| return ret; |
| } |
| |
| static bool can_write_data(struct io *io, void *user_data) |
| { |
| struct bt_att_chan *chan = user_data; |
| struct att_send_op *op; |
| struct timeout_data *timeout; |
| |
| op = pick_next_send_op(chan); |
| if (!op) |
| return false; |
| |
| if (bt_att_chan_write(chan, op->opcode, op->pdu, op->len) < 0) { |
| if (op->callback) |
| op->callback(BT_ATT_OP_ERROR_RSP, NULL, 0, |
| op->user_data); |
| destroy_att_send_op(op); |
| return true; |
| } |
| |
| /* Based on the operation type, set either the pending request or the |
| * pending indication. If it came from the write queue, then there is |
| * no need to keep it around. |
| */ |
| switch (op->type) { |
| case ATT_OP_TYPE_REQ: |
| chan->pending_req = op; |
| break; |
| case ATT_OP_TYPE_IND: |
| chan->pending_ind = op; |
| break; |
| case ATT_OP_TYPE_RSP: |
| /* Set in_req to false to indicate that no request is pending */ |
| chan->in_req = false; |
| /* fall through */ |
| case ATT_OP_TYPE_CMD: |
| case ATT_OP_TYPE_NFY: |
| case ATT_OP_TYPE_CONF: |
| case ATT_OP_TYPE_UNKNOWN: |
| default: |
| destroy_att_send_op(op); |
| return true; |
| } |
| |
| timeout = new0(struct timeout_data, 1); |
| timeout->chan = chan; |
| timeout->id = op->id; |
| op->timeout_id = timeout_add(ATT_TIMEOUT_INTERVAL, timeout_cb, |
| timeout, free); |
| |
| /* Return true as there may be more operations ready to write. */ |
| return true; |
| } |
| |
| static void wakeup_chan_writer(void *data, void *user_data) |
| { |
| struct bt_att_chan *chan = data; |
| struct bt_att *att = chan->att; |
| |
| if (chan->writer_active) |
| return; |
| |
| /* Set the write handler only if there is anything that can be sent |
| * at all. |
| */ |
| if (queue_isempty(chan->queue) && queue_isempty(att->write_queue)) { |
| if ((chan->pending_req || queue_isempty(att->req_queue)) && |
| (chan->pending_ind || queue_isempty(att->ind_queue))) |
| return; |
| } |
| |
| if (!io_set_write_handler(chan->io, can_write_data, chan, |
| write_watch_destroy)) |
| return; |
| |
| chan->writer_active = true; |
| } |
| |
| static void wakeup_writer(struct bt_att *att) |
| { |
| queue_foreach(att->chans, wakeup_chan_writer, NULL); |
| } |
| |
| static void disconn_handler(void *data, void *user_data) |
| { |
| struct att_disconn *disconn = data; |
| int err = PTR_TO_INT(user_data); |
| |
| if (disconn->removed) |
| return; |
| |
| if (disconn->callback) |
| disconn->callback(err, disconn->user_data); |
| } |
| |
| static void bt_att_chan_free(void *data) |
| { |
| struct bt_att_chan *chan = data; |
| |
| if (chan->pending_req) |
| destroy_att_send_op(chan->pending_req); |
| |
| if (chan->pending_ind) |
| destroy_att_send_op(chan->pending_ind); |
| |
| if (chan->pending_db_sync) |
| destroy_att_send_op(chan->pending_db_sync); |
| |
| queue_destroy(chan->queue, destroy_att_send_op); |
| |
| io_destroy(chan->io); |
| |
| free(chan->buf); |
| free(chan); |
| } |
| |
| static bool disconnect_cb(struct io *io, void *user_data) |
| { |
| struct bt_att_chan *chan = user_data; |
| struct bt_att *att = chan->att; |
| int err; |
| socklen_t len; |
| |
| len = sizeof(err); |
| |
| if (getsockopt(chan->fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) { |
| DBG(att, "(chan %p) Failed to obtain disconnect " |
| "error: %s", chan, strerror(errno)); |
| err = 0; |
| } |
| |
| DBG(att, "Channel %p disconnected: %s", chan, strerror(err)); |
| |
| /* Detach channel */ |
| queue_remove(att->chans, chan); |
| |
| if (chan->pending_req) { |
| disc_att_send_op(chan->pending_req); |
| chan->pending_req = NULL; |
| } |
| |
| if (chan->pending_ind) { |
| disc_att_send_op(chan->pending_ind); |
| chan->pending_ind = NULL; |
| } |
| |
| if (chan->pending_db_sync) { |
| disc_att_send_op(chan->pending_db_sync); |
| chan->pending_db_sync = NULL; |
| } |
| |
| bt_att_chan_free(chan); |
| |
| /* Don't run disconnect callback if there are channels left */ |
| if (!queue_isempty(att->chans)) |
| return false; |
| |
| bt_att_ref(att); |
| |
| att->in_disc = true; |
| |
| /* Notify request callbacks */ |
| queue_remove_all(att->req_queue, NULL, NULL, disc_att_send_op); |
| queue_remove_all(att->ind_queue, NULL, NULL, disc_att_send_op); |
| queue_remove_all(att->write_queue, NULL, NULL, disc_att_send_op); |
| |
| att->in_disc = false; |
| |
| queue_foreach(att->disconn_list, disconn_handler, INT_TO_PTR(err)); |
| |
| bt_att_unregister_all(att); |
| bt_att_unref(att); |
| |
| return false; |
| } |
| |
| static int bt_att_chan_get_security(struct bt_att_chan *chan) |
| { |
| struct bt_security sec; |
| socklen_t len; |
| |
| if (chan->type == BT_ATT_LOCAL) |
| return chan->sec_level; |
| |
| memset(&sec, 0, sizeof(sec)); |
| len = sizeof(sec); |
| if (getsockopt(chan->fd, SOL_BLUETOOTH, BT_SECURITY, &sec, &len) < 0) |
| return -EIO; |
| |
| return sec.level; |
| } |
| |
| static bool bt_att_chan_set_security(struct bt_att_chan *chan, int level) |
| { |
| struct bt_security sec; |
| |
| if (chan->type == BT_ATT_LOCAL) { |
| chan->sec_level = level; |
| return true; |
| } |
| |
| /* Check if security level has already been set, if the security level |
| * is higher it shall satisfy the request since we never want to |
| * downgrade security. |
| */ |
| if (level <= bt_att_chan_get_security(chan)) |
| return true; |
| |
| memset(&sec, 0, sizeof(sec)); |
| sec.level = level; |
| |
| if (setsockopt(chan->fd, SOL_BLUETOOTH, BT_SECURITY, &sec, |
| sizeof(sec)) < 0) |
| return false; |
| |
| return true; |
| } |
| |
| static bool change_security(struct bt_att_chan *chan, uint8_t ecode) |
| { |
| int security; |
| |
| if (chan->sec_level != BT_ATT_SECURITY_AUTO) |
| return false; |
| |
| security = bt_att_chan_get_security(chan); |
| |
| if (ecode == BT_ATT_ERROR_INSUFFICIENT_ENCRYPTION && |
| security < BT_ATT_SECURITY_MEDIUM) { |
| security = BT_ATT_SECURITY_MEDIUM; |
| } else if (ecode == BT_ATT_ERROR_AUTHENTICATION) { |
| if (security < BT_ATT_SECURITY_MEDIUM) |
| security = BT_ATT_SECURITY_MEDIUM; |
| else if (security < BT_ATT_SECURITY_HIGH) |
| security = BT_ATT_SECURITY_HIGH; |
| else if (security < BT_ATT_SECURITY_FIPS) |
| security = BT_ATT_SECURITY_FIPS; |
| else |
| return false; |
| } else { |
| return false; |
| } |
| |
| return bt_att_chan_set_security(chan, security); |
| } |
| |
| static bool handle_error_rsp(struct bt_att_chan *chan, uint8_t *pdu, |
| ssize_t pdu_len, uint8_t *opcode) |
| { |
| struct bt_att *att = chan->att; |
| const struct bt_att_pdu_error_rsp *rsp; |
| struct att_send_op *op = chan->pending_req; |
| |
| if (pdu_len != sizeof(*rsp)) { |
| *opcode = 0; |
| return false; |
| } |
| |
| rsp = (void *) pdu; |
| |
| *opcode = rsp->opcode; |
| |
| /* If operation has already been marked as retry don't attempt to change |
| * the security again. |
| */ |
| if (op->retry) |
| return false; |
| |
| /* Attempt to change security */ |
| if (change_security(chan, rsp->ecode)) |
| goto retry; |
| |
| /* Check if this is DB_OUT_OF_SYNC and we have a callback */ |
| if (rsp->ecode == BT_ATT_ERROR_DB_OUT_OF_SYNC && |
| att->db_sync_callback) { |
| /* Remove timeout since we're waiting for approval */ |
| if (op->timeout_id) { |
| timeout_remove(op->timeout_id); |
| op->timeout_id = 0; |
| } |
| |
| /* Move to pending_db_sync */ |
| chan->pending_db_sync = op; |
| chan->pending_req = NULL; |
| |
| DBG(att, "(chan %p) DB out of sync for operation %p", chan, op); |
| |
| /* Notify upper layer */ |
| att->db_sync_callback(rsp, op->pdu + 1, op->len - 1, |
| op->id, att->db_sync_data); |
| |
| return true; |
| } |
| |
| return false; |
| |
| retry: |
| /* Remove timeout_id if outstanding */ |
| if (op->timeout_id) { |
| timeout_remove(op->timeout_id); |
| op->timeout_id = 0; |
| } |
| |
| DBG(att, "(chan %p) Retrying operation %p", chan, op); |
| |
| chan->pending_req = NULL; |
| op->retry = true; |
| |
| /* Push operation back to channel queue */ |
| return queue_push_head(chan->queue, op); |
| } |
| |
| static void handle_rsp(struct bt_att_chan *chan, uint8_t opcode, uint8_t *pdu, |
| ssize_t pdu_len) |
| { |
| struct bt_att *att = chan->att; |
| struct att_send_op *op = chan->pending_req; |
| uint8_t req_opcode; |
| uint8_t rsp_opcode; |
| uint8_t *rsp_pdu = NULL; |
| uint16_t rsp_pdu_len = 0; |
| |
| /* |
| * If no request is pending, then the response is unexpected. Disconnect |
| * the bearer. |
| */ |
| if (!op) { |
| DBG(att, "(chan %p) Received unexpected ATT response", |
| chan); |
| io_shutdown(chan->io); |
| return; |
| } |
| |
| /* |
| * If the received response doesn't match the pending request, or if |
| * the request is malformed, end the current request with failure. |
| */ |
| if (opcode == BT_ATT_OP_ERROR_RSP) { |
| /* Return if error response cause a retry */ |
| if (handle_error_rsp(chan, pdu, pdu_len, &req_opcode)) { |
| wakeup_chan_writer(chan, NULL); |
| return; |
| } |
| } else if (!(req_opcode = get_req_opcode(opcode))) |
| goto fail; |
| |
| if (req_opcode != op->opcode) |
| goto fail; |
| |
| rsp_opcode = opcode; |
| |
| if (pdu_len > 0) { |
| rsp_pdu = pdu; |
| rsp_pdu_len = pdu_len; |
| } |
| |
| goto done; |
| |
| fail: |
| DBG(att, "(chan %p) Failed to handle response PDU; opcode: " |
| "0x%02x", chan, opcode); |
| |
| rsp_opcode = BT_ATT_OP_ERROR_RSP; |
| |
| done: |
| if (op->callback) |
| op->callback(rsp_opcode, rsp_pdu, rsp_pdu_len, op->user_data); |
| |
| destroy_att_send_op(op); |
| chan->pending_req = NULL; |
| |
| wakeup_chan_writer(chan, NULL); |
| } |
| |
| static void handle_conf(struct bt_att_chan *chan, uint8_t *pdu, ssize_t pdu_len) |
| { |
| struct bt_att *att = chan->att; |
| struct att_send_op *op = chan->pending_ind; |
| |
| /* |
| * Disconnect the bearer if the confirmation is unexpected or the PDU is |
| * invalid. |
| */ |
| if (!op || pdu_len) { |
| DBG(att, "(chan %p) Received unexpected/invalid ATT " |
| "confirmation", chan); |
| io_shutdown(chan->io); |
| return; |
| } |
| |
| if (op->callback) |
| op->callback(BT_ATT_OP_HANDLE_CONF, NULL, 0, op->user_data); |
| |
| destroy_att_send_op(op); |
| chan->pending_ind = NULL; |
| |
| wakeup_chan_writer(chan, NULL); |
| } |
| |
| struct notify_data { |
| uint8_t opcode; |
| uint8_t *pdu; |
| ssize_t pdu_len; |
| bool handler_found; |
| }; |
| |
| static bool opcode_match(uint8_t opcode, uint8_t test_opcode) |
| { |
| enum att_op_type op_type = get_op_type(test_opcode); |
| |
| if (opcode == BT_ATT_ALL_REQUESTS && (op_type == ATT_OP_TYPE_REQ || |
| op_type == ATT_OP_TYPE_CMD)) |
| return true; |
| |
| return opcode == test_opcode; |
| } |
| |
| static void respond_not_supported(struct bt_att *att, uint8_t opcode) |
| { |
| struct bt_att_pdu_error_rsp pdu; |
| |
| pdu.opcode = opcode; |
| pdu.handle = 0x0000; |
| pdu.ecode = BT_ATT_ERROR_REQUEST_NOT_SUPPORTED; |
| |
| bt_att_send(att, BT_ATT_OP_ERROR_RSP, &pdu, sizeof(pdu), NULL, NULL, |
| NULL); |
| } |
| |
| static bool handle_signed(struct bt_att *att, uint8_t *pdu, ssize_t pdu_len) |
| { |
| uint8_t *signature; |
| uint32_t sign_cnt; |
| struct sign_info *sign; |
| uint8_t opcode = pdu[0]; |
| |
| /* Check if there is enough data for a signature */ |
| if (pdu_len < 3 + BT_ATT_SIGNATURE_LEN) |
| goto fail; |
| |
| sign = att->remote_sign; |
| if (!sign) |
| goto fail; |
| |
| signature = pdu + (pdu_len - BT_ATT_SIGNATURE_LEN); |
| sign_cnt = get_le32(signature); |
| |
| /* Validate counter */ |
| if (!sign->counter(&sign_cnt, sign->user_data)) |
| goto fail; |
| |
| /* Verify received signature */ |
| if (!bt_crypto_verify_att_sign(att->crypto, sign->key, pdu, pdu_len)) |
| goto fail; |
| |
| return true; |
| |
| fail: |
| DBG(att, "ATT failed to verify signature: 0x%02x", opcode); |
| |
| return false; |
| } |
| |
| static void handle_notify(struct bt_att_chan *chan, uint8_t *pdu, |
| ssize_t pdu_len) |
| { |
| struct bt_att *att = chan->att; |
| const struct queue_entry *entry; |
| bool found; |
| uint8_t opcode = pdu[0]; |
| |
| bt_att_ref(att); |
| |
| found = false; |
| entry = queue_get_entries(att->notify_list); |
| |
| while (entry) { |
| struct att_notify *notify = entry->data; |
| |
| entry = entry->next; |
| |
| if (!opcode_match(notify->opcode, opcode)) |
| continue; |
| |
| if ((opcode & ATT_OP_SIGNED_MASK) && att->crypto) { |
| if (!handle_signed(att, pdu, pdu_len)) |
| return; |
| pdu_len -= BT_ATT_SIGNATURE_LEN; |
| } |
| |
| /* BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 3, Part G |
| * page 2370 |
| * |
| * 4.3.1 Exchange MTU |
| * |
| * This sub-procedure shall not be used on a BR/EDR physical |
| * link since the MTU size is negotiated using L2CAP channel |
| * configuration procedures. |
| */ |
| if (bt_att_get_link_type(att) == BT_ATT_BREDR || |
| chan->type == BT_ATT_EATT) { |
| switch (opcode) { |
| case BT_ATT_OP_MTU_REQ: |
| goto not_supported; |
| } |
| } |
| |
| found = true; |
| |
| if (notify->callback) |
| notify->callback(chan, chan->mtu, opcode, |
| pdu + 1, pdu_len - 1, |
| notify->user_data); |
| |
| /* callback could remove all entries from notify list */ |
| if (queue_isempty(att->notify_list)) |
| break; |
| } |
| |
| not_supported: |
| /* |
| * If this was not a command and no handler was registered for it, |
| * respond with "Not Supported" |
| */ |
| if (!found && get_op_type(opcode) != ATT_OP_TYPE_CMD) |
| respond_not_supported(att, opcode); |
| |
| bt_att_unref(att); |
| } |
| |
| static bool can_read_data(struct io *io, void *user_data) |
| { |
| struct bt_att_chan *chan = user_data; |
| struct bt_att *att = chan->att; |
| uint8_t opcode; |
| uint8_t *pdu; |
| ssize_t bytes_read; |
| |
| bytes_read = read(chan->fd, chan->buf, chan->mtu); |
| if (bytes_read < 0) |
| return false; |
| |
| VERBOSE(att, "(chan %p) ATT received: %zd", chan, bytes_read); |
| |
| att_hexdump(att, '>', chan->buf, bytes_read); |
| |
| if (bytes_read < ATT_MIN_PDU_LEN) |
| return true; |
| |
| pdu = chan->buf; |
| opcode = pdu[0]; |
| |
| bt_att_ref(att); |
| |
| /* Act on the received PDU based on the opcode type */ |
| switch (get_op_type(opcode)) { |
| case ATT_OP_TYPE_RSP: |
| VERBOSE(att, "(chan %p) ATT response received: 0x%02x", |
| chan, opcode); |
| handle_rsp(chan, opcode, pdu + 1, bytes_read - 1); |
| break; |
| case ATT_OP_TYPE_CONF: |
| VERBOSE(att, "(chan %p) ATT confirmation received: 0x%02x", |
| chan, opcode); |
| handle_conf(chan, pdu + 1, bytes_read - 1); |
| break; |
| case ATT_OP_TYPE_REQ: |
| /* |
| * If a request is currently pending, then the sequential |
| * protocol was violated. Disconnect the bearer, which will |
| * promptly notify the upper layer via disconnect handlers. |
| */ |
| if (chan->in_req) { |
| DBG(att, "(chan %p) Received request while " |
| "another is pending: 0x%02x", |
| chan, opcode); |
| io_shutdown(chan->io); |
| bt_att_unref(chan->att); |
| |
| return false; |
| } |
| |
| chan->in_req = true; |
| /* fall through */ |
| case ATT_OP_TYPE_CMD: |
| case ATT_OP_TYPE_NFY: |
| case ATT_OP_TYPE_UNKNOWN: |
| case ATT_OP_TYPE_IND: |
| /* fall through */ |
| default: |
| /* For all other opcodes notify the upper layer of the PDU and |
| * let them act on it. |
| */ |
| DBG(att, "(chan %p) ATT PDU received: 0x%02x", chan, |
| opcode); |
| handle_notify(chan, pdu, bytes_read); |
| break; |
| } |
| |
| bt_att_unref(att); |
| |
| return true; |
| } |
| |
| static bool is_io_l2cap_based(int fd) |
| { |
| int domain; |
| int proto; |
| int err; |
| socklen_t len; |
| |
| domain = 0; |
| len = sizeof(domain); |
| err = getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &len); |
| if (err < 0) |
| return false; |
| |
| if (domain != AF_BLUETOOTH) |
| return false; |
| |
| proto = 0; |
| len = sizeof(proto); |
| err = getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &proto, &len); |
| if (err < 0) |
| return false; |
| |
| return proto == BTPROTO_L2CAP; |
| } |
| |
| static void bt_att_free(struct bt_att *att) |
| { |
| bt_crypto_unref(att->crypto); |
| |
| if (att->timeout_destroy) |
| att->timeout_destroy(att->timeout_data); |
| |
| if (att->db_sync_destroy) |
| att->db_sync_destroy(att->db_sync_data); |
| |
| if (att->debug_destroy) |
| att->debug_destroy(att->debug_data); |
| |
| free(att->local_sign); |
| free(att->remote_sign); |
| |
| queue_destroy(att->req_queue, NULL); |
| queue_destroy(att->ind_queue, NULL); |
| queue_destroy(att->write_queue, NULL); |
| queue_destroy(att->notify_list, NULL); |
| queue_destroy(att->disconn_list, NULL); |
| queue_destroy(att->exchange_list, NULL); |
| queue_destroy(att->chans, bt_att_chan_free); |
| |
| free(att); |
| } |
| |
| static uint16_t io_get_mtu(int fd) |
| { |
| socklen_t len; |
| struct l2cap_options l2o; |
| |
| len = sizeof(l2o); |
| if (!getsockopt(fd, SOL_L2CAP, L2CAP_OPTIONS, &l2o, &len)) |
| return l2o.omtu; |
| |
| if (!getsockopt(fd, SOL_BLUETOOTH, BT_SNDMTU, &l2o.omtu, &len)) |
| return l2o.omtu; |
| |
| return 0; |
| } |
| |
| static uint8_t io_get_type(int fd) |
| { |
| struct sockaddr_l2 src; |
| socklen_t len; |
| |
| if (!is_io_l2cap_based(fd)) |
| return BT_ATT_LOCAL; |
| |
| len = sizeof(src); |
| memset(&src, 0, len); |
| if (getsockname(fd, (void *)&src, &len) < 0) |
| return -errno; |
| |
| if (src.l2_bdaddr_type == BDADDR_BREDR) |
| return BT_ATT_BREDR; |
| |
| return BT_ATT_LE; |
| } |
| |
| static struct bt_att_chan *bt_att_chan_new(int fd, uint8_t type) |
| { |
| struct bt_att_chan *chan; |
| |
| if (fd < 0) |
| return NULL; |
| |
| chan = new0(struct bt_att_chan, 1); |
| chan->fd = fd; |
| |
| chan->io = io_new(fd); |
| if (!chan->io) |
| goto fail; |
| |
| if (!io_set_read_handler(chan->io, can_read_data, chan, NULL)) |
| goto fail; |
| |
| if (!io_set_disconnect_handler(chan->io, disconnect_cb, chan, NULL)) |
| goto fail; |
| |
| chan->type = type; |
| switch (chan->type) { |
| case BT_ATT_LOCAL: |
| chan->sec_level = BT_ATT_SECURITY_LOW; |
| /* fall through */ |
| case BT_ATT_LE: |
| chan->mtu = BT_ATT_DEFAULT_LE_MTU; |
| break; |
| default: |
| chan->mtu = io_get_mtu(chan->fd); |
| } |
| |
| if (chan->mtu < BT_ATT_DEFAULT_LE_MTU) |
| goto fail; |
| |
| chan->buf = malloc(chan->mtu); |
| if (!chan->buf) |
| goto fail; |
| |
| chan->queue = queue_new(); |
| |
| return chan; |
| |
| fail: |
| bt_att_chan_free(chan); |
| |
| return NULL; |
| } |
| |
| static void bt_att_attach_chan(struct bt_att *att, struct bt_att_chan *chan) |
| { |
| /* Push to head as EATT channels have higher priority */ |
| queue_push_head(att->chans, chan); |
| chan->att = att; |
| |
| if (chan->mtu > att->mtu) |
| att->mtu = chan->mtu; |
| |
| io_set_close_on_destroy(chan->io, att->close_on_unref); |
| |
| DBG(att, "Channel %p attached", chan); |
| |
| wakeup_chan_writer(chan, NULL); |
| } |
| |
| struct bt_att *bt_att_new(int fd, bool ext_signed) |
| { |
| struct bt_att *att; |
| struct bt_att_chan *chan; |
| |
| chan = bt_att_chan_new(fd, io_get_type(fd)); |
| if (!chan) |
| return NULL; |
| |
| att = new0(struct bt_att, 1); |
| att->chans = queue_new(); |
| att->mtu = chan->mtu; |
| |
| /* crypto is optional, if not available leave it NULL */ |
| if (!ext_signed) |
| att->crypto = bt_crypto_new(); |
| |
| att->req_queue = queue_new(); |
| att->ind_queue = queue_new(); |
| att->write_queue = queue_new(); |
| att->notify_list = queue_new(); |
| att->disconn_list = queue_new(); |
| att->exchange_list = queue_new(); |
| |
| bt_att_attach_chan(att, chan); |
| |
| return bt_att_ref(att); |
| } |
| |
| struct bt_att *bt_att_ref(struct bt_att *att) |
| { |
| if (!att) |
| return NULL; |
| |
| __sync_fetch_and_add(&att->ref_count, 1); |
| |
| return att; |
| } |
| |
| void bt_att_unref(struct bt_att *att) |
| { |
| if (!att) |
| return; |
| |
| if (__sync_sub_and_fetch(&att->ref_count, 1)) |
| return; |
| |
| bt_att_unregister_all(att); |
| bt_att_cancel_all(att); |
| |
| bt_att_free(att); |
| } |
| |
| bool bt_att_set_close_on_unref(struct bt_att *att, bool do_close) |
| { |
| const struct queue_entry *entry; |
| |
| if (!att) |
| return false; |
| |
| att->close_on_unref = do_close; |
| |
| for (entry = queue_get_entries(att->chans); entry; |
| entry = entry->next) { |
| struct bt_att_chan *chan = entry->data; |
| |
| if (!io_set_close_on_destroy(chan->io, do_close)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| int bt_att_attach_fd(struct bt_att *att, int fd) |
| { |
| struct bt_att_chan *chan; |
| |
| if (!att || fd < 0) |
| return -EINVAL; |
| |
| chan = bt_att_chan_new(fd, BT_ATT_EATT); |
| if (!chan) |
| return -EINVAL; |
| |
| bt_att_attach_chan(att, chan); |
| |
| return 0; |
| } |
| |
| int bt_att_get_fd(struct bt_att *att) |
| { |
| struct bt_att_chan *chan; |
| |
| if (!att) |
| return -1; |
| |
| if (queue_isempty(att->chans)) |
| return -ENOTCONN; |
| |
| chan = queue_peek_tail(att->chans); |
| |
| return chan->fd; |
| } |
| |
| int bt_att_get_channels(struct bt_att *att) |
| { |
| if (!att) |
| return 0; |
| |
| return queue_length(att->chans); |
| } |
| |
| bool bt_att_set_debug(struct bt_att *att, uint8_t level, |
| bt_att_debug_func_t callback, void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| if (!att) |
| return false; |
| |
| if (att->debug_destroy) |
| att->debug_destroy(att->debug_data); |
| |
| att->debug_level = level; |
| att->debug_callback = callback; |
| att->debug_destroy = destroy; |
| att->debug_data = user_data; |
| |
| return true; |
| } |
| |
| uint16_t bt_att_get_mtu(struct bt_att *att) |
| { |
| if (!att) |
| return 0; |
| |
| return att->mtu; |
| } |
| |
| static void exchange_handler(void *data, void *user_data) |
| { |
| struct att_exchange *exchange = data; |
| uint16_t mtu = PTR_TO_INT(user_data); |
| |
| if (exchange->removed) |
| return; |
| |
| if (exchange->callback) |
| exchange->callback(mtu, exchange->user_data); |
| } |
| |
| bool bt_att_set_mtu(struct bt_att *att, uint16_t mtu) |
| { |
| struct bt_att_chan *chan; |
| void *buf; |
| |
| if (!att) |
| return false; |
| |
| if (mtu < BT_ATT_DEFAULT_LE_MTU) |
| return false; |
| |
| /* Original channel is always the last */ |
| chan = queue_peek_tail(att->chans); |
| if (!chan) |
| return -ENOTCONN; |
| |
| buf = malloc(mtu); |
| if (!buf) |
| return false; |
| |
| free(chan->buf); |
| |
| chan->mtu = mtu; |
| chan->buf = buf; |
| |
| if (chan->mtu > att->mtu) { |
| att->mtu = chan->mtu; |
| queue_foreach(att->exchange_list, exchange_handler, |
| INT_TO_PTR(att->mtu)); |
| } |
| |
| return true; |
| } |
| |
| uint8_t bt_att_get_link_type(struct bt_att *att) |
| { |
| struct bt_att_chan *chan; |
| |
| if (!att) |
| return -EINVAL; |
| |
| chan = queue_peek_tail(att->chans); |
| if (!chan) |
| return -ENOTCONN; |
| |
| return chan->type; |
| } |
| |
| bool bt_att_set_timeout_cb(struct bt_att *att, bt_att_timeout_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| if (!att) |
| return false; |
| |
| if (att->timeout_destroy) |
| att->timeout_destroy(att->timeout_data); |
| |
| att->timeout_callback = callback; |
| att->timeout_destroy = destroy; |
| att->timeout_data = user_data; |
| |
| return true; |
| } |
| |
| bool bt_att_set_db_sync_cb(struct bt_att *att, bt_att_db_sync_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| if (!att) |
| return false; |
| |
| if (att->db_sync_destroy) |
| att->db_sync_destroy(att->db_sync_data); |
| |
| att->db_sync_callback = callback; |
| att->db_sync_destroy = destroy; |
| att->db_sync_data = user_data; |
| |
| return true; |
| } |
| |
| unsigned int bt_att_register_disconnect(struct bt_att *att, |
| bt_att_disconnect_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_disconn *disconn; |
| |
| if (!att || queue_isempty(att->chans)) |
| return 0; |
| |
| disconn = new0(struct att_disconn, 1); |
| disconn->callback = callback; |
| disconn->destroy = destroy; |
| disconn->user_data = user_data; |
| |
| if (att->next_reg_id < 1) |
| att->next_reg_id = 1; |
| |
| disconn->id = att->next_reg_id++; |
| |
| if (!queue_push_tail(att->disconn_list, disconn)) { |
| free(disconn); |
| return 0; |
| } |
| |
| return disconn->id; |
| } |
| |
| bool bt_att_unregister_disconnect(struct bt_att *att, unsigned int id) |
| { |
| struct att_disconn *disconn; |
| |
| if (!att || !id) |
| return false; |
| |
| /* Check if disconnect is running */ |
| if (queue_isempty(att->chans)) { |
| disconn = queue_find(att->disconn_list, match_disconn_id, |
| UINT_TO_PTR(id)); |
| if (!disconn) |
| return false; |
| |
| disconn->removed = true; |
| return true; |
| } |
| |
| disconn = queue_remove_if(att->disconn_list, match_disconn_id, |
| UINT_TO_PTR(id)); |
| if (!disconn) |
| return false; |
| |
| destroy_att_disconn(disconn); |
| return true; |
| } |
| |
| unsigned int bt_att_register_exchange(struct bt_att *att, |
| bt_att_exchange_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_exchange *mtu; |
| |
| if (!att || queue_isempty(att->chans)) |
| return 0; |
| |
| mtu = new0(struct att_exchange, 1); |
| mtu->callback = callback; |
| mtu->destroy = destroy; |
| mtu->user_data = user_data; |
| |
| if (att->next_reg_id < 1) |
| att->next_reg_id = 1; |
| |
| mtu->id = att->next_reg_id++; |
| |
| if (!queue_push_tail(att->exchange_list, mtu)) { |
| free(att); |
| return 0; |
| } |
| |
| return mtu->id; |
| } |
| |
| bool bt_att_unregister_exchange(struct bt_att *att, unsigned int id) |
| { |
| struct att_exchange *mtu; |
| |
| if (!att || !id) |
| return false; |
| |
| /* Check if disconnect is running */ |
| if (queue_isempty(att->chans)) { |
| mtu = queue_find(att->exchange_list, match_disconn_id, |
| UINT_TO_PTR(id)); |
| if (!mtu) |
| return false; |
| |
| mtu->removed = true; |
| return true; |
| } |
| |
| mtu = queue_remove_if(att->exchange_list, match_disconn_id, |
| UINT_TO_PTR(id)); |
| if (!mtu) |
| return false; |
| |
| destroy_att_exchange(mtu); |
| return true; |
| } |
| |
| unsigned int bt_att_send(struct bt_att *att, uint8_t opcode, |
| const void *pdu, uint16_t length, |
| bt_att_response_func_t callback, void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_send_op *op; |
| bool result; |
| |
| if (!att || queue_isempty(att->chans)) |
| return 0; |
| |
| op = create_att_send_op(att, opcode, pdu, length, callback, user_data, |
| destroy); |
| if (!op) |
| return 0; |
| |
| if (att->next_send_id < 1) |
| att->next_send_id = 1; |
| |
| op->id = att->next_send_id++; |
| |
| /* Always use fixed channel for BT_ATT_OP_MTU_REQ */ |
| if (opcode == BT_ATT_OP_MTU_REQ) { |
| struct bt_att_chan *chan = queue_peek_tail(att->chans); |
| |
| result = queue_push_tail(chan->queue, op); |
| goto done; |
| } |
| |
| /* Add the op to the correct queue based on its type */ |
| switch (op->type) { |
| case ATT_OP_TYPE_REQ: |
| result = queue_push_tail(att->req_queue, op); |
| break; |
| case ATT_OP_TYPE_IND: |
| result = queue_push_tail(att->ind_queue, op); |
| break; |
| case ATT_OP_TYPE_CMD: |
| case ATT_OP_TYPE_NFY: |
| case ATT_OP_TYPE_UNKNOWN: |
| case ATT_OP_TYPE_RSP: |
| case ATT_OP_TYPE_CONF: |
| default: |
| result = queue_push_tail(att->write_queue, op); |
| break; |
| } |
| |
| done: |
| if (!result) { |
| free(op->pdu); |
| free(op); |
| return 0; |
| } |
| |
| wakeup_writer(att); |
| |
| return op->id; |
| } |
| |
| int bt_att_resend(struct bt_att *att, unsigned int id, uint8_t opcode, |
| const void *pdu, uint16_t length, |
| bt_att_response_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| const struct queue_entry *entry; |
| struct att_send_op *op; |
| bool result; |
| |
| if (!att || !id) |
| return -EINVAL; |
| |
| /* Lookup request on each channel */ |
| for (entry = queue_get_entries(att->chans); entry; |
| entry = entry->next) { |
| struct bt_att_chan *chan = entry->data; |
| |
| if (chan->pending_req && chan->pending_req->id == id) |
| break; |
| |
| /* Also check pending_db_sync */ |
| if (chan->pending_db_sync && chan->pending_db_sync->id == id) { |
| op = chan->pending_db_sync; |
| chan->pending_db_sync = NULL; |
| DBG(att, "(chan %p) Resending DB out of sync operation" |
| " %p", chan, op); |
| goto done; |
| } |
| } |
| |
| if (!entry) |
| return -ENOENT; |
| |
| /* Only allow requests to be resend */ |
| if (get_op_type(opcode) != ATT_OP_TYPE_REQ) |
| return -EOPNOTSUPP; |
| |
| op = create_att_send_op(att, opcode, pdu, length, callback, user_data, |
| destroy); |
| if (!op) |
| return -ENOMEM; |
| |
| op->id = id; |
| |
| done: |
| switch (opcode) { |
| /* Only prepend requests that could be a continuation */ |
| case BT_ATT_OP_READ_BLOB_REQ: |
| case BT_ATT_OP_PREP_WRITE_REQ: |
| case BT_ATT_OP_EXEC_WRITE_REQ: |
| result = queue_push_head(att->req_queue, op); |
| break; |
| default: |
| result = queue_push_tail(att->req_queue, op); |
| break; |
| } |
| |
| if (!result) { |
| free(op->pdu); |
| free(op); |
| return -ENOMEM; |
| } |
| |
| wakeup_writer(att); |
| |
| return 0; |
| } |
| |
| unsigned int bt_att_chan_send(struct bt_att_chan *chan, uint8_t opcode, |
| const void *pdu, uint16_t len, |
| bt_att_response_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_send_op *op; |
| |
| if (!chan || !chan->att) |
| return -EINVAL; |
| |
| op = create_att_send_op(chan->att, opcode, pdu, len, callback, |
| user_data, destroy); |
| if (!op) |
| return -EINVAL; |
| |
| if (!queue_push_tail(chan->queue, op)) { |
| free(op->pdu); |
| free(op); |
| return 0; |
| } |
| |
| wakeup_chan_writer(chan, NULL); |
| |
| return op->id; |
| } |
| |
| static bool match_op_id(const void *a, const void *b) |
| { |
| const struct att_send_op *op = a; |
| unsigned int id = PTR_TO_UINT(b); |
| |
| return op->id == id; |
| } |
| |
| bool bt_att_chan_cancel(struct bt_att_chan *chan, unsigned int id) |
| { |
| struct att_send_op *op; |
| |
| if (chan->pending_req && chan->pending_req->id == id) { |
| /* Don't cancel the pending request; remove it's handlers */ |
| cancel_att_send_op(chan->pending_req); |
| return true; |
| } |
| |
| if (chan->pending_ind && chan->pending_ind->id == id) { |
| /* Don't cancel the pending indication; remove it's handlers. */ |
| cancel_att_send_op(chan->pending_ind); |
| return true; |
| } |
| |
| op = queue_remove_if(chan->queue, match_op_id, UINT_TO_PTR(id)); |
| if (!op) |
| return false; |
| |
| destroy_att_send_op(op); |
| |
| wakeup_chan_writer(chan, NULL); |
| |
| return true; |
| } |
| |
| static bool bt_att_db_sync_cancel(struct bt_att_chan *chan, unsigned int id) |
| { |
| struct att_send_op *op = chan->pending_db_sync; |
| struct bt_att_pdu_error_rsp rsp; |
| |
| if (!op || op->id != id) |
| return false; |
| |
| /* Build error response PDU */ |
| memset(&rsp, 0, sizeof(rsp)); |
| rsp.opcode = op->opcode; |
| rsp.ecode = BT_ATT_ERROR_DB_OUT_OF_SYNC; |
| |
| /* Clear pending state */ |
| chan->pending_db_sync = NULL; |
| |
| /* Notify callback with error */ |
| if (op->callback) |
| op->callback(BT_ATT_OP_ERROR_RSP, &rsp, sizeof(rsp), |
| op->user_data); |
| |
| destroy_att_send_op(op); |
| wakeup_chan_writer(chan, NULL); |
| |
| return true; |
| } |
| |
| static bool bt_att_disc_cancel(struct bt_att *att, unsigned int id) |
| { |
| struct att_send_op *op; |
| |
| op = queue_find(att->req_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_find(att->ind_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_find(att->write_queue, match_op_id, UINT_TO_PTR(id)); |
| |
| done: |
| if (!op) |
| return false; |
| |
| /* Just cancel since disconnect_cb will be cleaning up */ |
| cancel_att_send_op(op); |
| |
| return true; |
| } |
| |
| bool bt_att_cancel(struct bt_att *att, unsigned int id) |
| { |
| const struct queue_entry *entry; |
| struct att_send_op *op; |
| |
| if (!att || !id) |
| return false; |
| |
| /* Lookup request on each channel first */ |
| for (entry = queue_get_entries(att->chans); entry; |
| entry = entry->next) { |
| struct bt_att_chan *chan = entry->data; |
| |
| /* Check pending_db_sync first on each channel */ |
| if (bt_att_db_sync_cancel(chan, id)) |
| return true; |
| |
| if (bt_att_chan_cancel(chan, id)) |
| return true; |
| } |
| |
| if (att->in_disc) |
| return bt_att_disc_cancel(att, id); |
| |
| op = queue_remove_if(att->req_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_remove_if(att->ind_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_remove_if(att->write_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| if (!op) |
| return false; |
| |
| done: |
| destroy_att_send_op(op); |
| |
| wakeup_writer(att); |
| |
| return true; |
| } |
| |
| bool bt_att_cancel_all(struct bt_att *att) |
| { |
| const struct queue_entry *entry; |
| |
| if (!att) |
| return false; |
| |
| queue_remove_all(att->req_queue, NULL, NULL, destroy_att_send_op); |
| queue_remove_all(att->ind_queue, NULL, NULL, destroy_att_send_op); |
| queue_remove_all(att->write_queue, NULL, NULL, destroy_att_send_op); |
| |
| for (entry = queue_get_entries(att->chans); entry; |
| entry = entry->next) { |
| struct bt_att_chan *chan = entry->data; |
| |
| if (chan->pending_req) |
| /* Don't cancel the pending request; remove it's |
| * handlers |
| */ |
| cancel_att_send_op(chan->pending_req); |
| |
| if (chan->pending_ind) |
| /* Don't cancel the pending request; remove it's |
| * handlers |
| */ |
| cancel_att_send_op(chan->pending_ind); |
| } |
| |
| return true; |
| } |
| |
| static uint8_t att_ecode_from_error(int err) |
| { |
| /* |
| * If the error fits in a single byte, treat it as an ATT protocol |
| * error as is. Since "0" is not a valid ATT protocol error code, we map |
| * that to UNLIKELY below. |
| */ |
| if (err > 0 && err < UINT8_MAX) |
| return err; |
| |
| /* |
| * Since we allow UNIX errnos, map them to appropriate ATT protocol |
| * and "Common Profile and Service" error codes. |
| */ |
| switch (err) { |
| case -ENOENT: |
| return BT_ATT_ERROR_INVALID_HANDLE; |
| case -ENOMEM: |
| return BT_ATT_ERROR_INSUFFICIENT_RESOURCES; |
| case -EALREADY: |
| return BT_ERROR_ALREADY_IN_PROGRESS; |
| case -EOVERFLOW: |
| return BT_ERROR_OUT_OF_RANGE; |
| } |
| |
| return BT_ATT_ERROR_UNLIKELY; |
| } |
| |
| int bt_att_chan_send_error_rsp(struct bt_att_chan *chan, uint8_t opcode, |
| uint16_t handle, int error) |
| { |
| struct bt_att_pdu_error_rsp pdu; |
| uint8_t ecode; |
| |
| if (!chan || !chan->att || !opcode) |
| return -EINVAL; |
| |
| ecode = att_ecode_from_error(error); |
| |
| memset(&pdu, 0, sizeof(pdu)); |
| |
| pdu.opcode = opcode; |
| put_le16(handle, &pdu.handle); |
| pdu.ecode = ecode; |
| |
| return bt_att_chan_send_rsp(chan, BT_ATT_OP_ERROR_RSP, &pdu, |
| sizeof(pdu)); |
| } |
| |
| unsigned int bt_att_register(struct bt_att *att, uint8_t opcode, |
| bt_att_notify_func_t callback, |
| void *user_data, |
| bt_att_destroy_func_t destroy) |
| { |
| struct att_notify *notify; |
| |
| if (!att || !callback || queue_isempty(att->chans)) |
| return 0; |
| |
| notify = new0(struct att_notify, 1); |
| notify->opcode = opcode; |
| notify->callback = callback; |
| notify->destroy = destroy; |
| notify->user_data = user_data; |
| |
| if (att->next_reg_id < 1) |
| att->next_reg_id = 1; |
| |
| notify->id = att->next_reg_id++; |
| |
| if (!queue_push_tail(att->notify_list, notify)) { |
| free(notify); |
| return 0; |
| } |
| |
| return notify->id; |
| } |
| |
| bool bt_att_unregister(struct bt_att *att, unsigned int id) |
| { |
| struct att_notify *notify; |
| |
| if (!att || !id) |
| return false; |
| |
| notify = queue_remove_if(att->notify_list, match_notify_id, |
| UINT_TO_PTR(id)); |
| if (!notify) |
| return false; |
| |
| destroy_att_notify(notify); |
| return true; |
| } |
| |
| bool bt_att_unregister_all(struct bt_att *att) |
| { |
| if (!att) |
| return false; |
| |
| queue_remove_all(att->notify_list, NULL, NULL, destroy_att_notify); |
| queue_remove_all(att->disconn_list, NULL, NULL, destroy_att_disconn); |
| queue_remove_all(att->exchange_list, NULL, NULL, destroy_att_exchange); |
| |
| return true; |
| } |
| |
| int bt_att_get_security(struct bt_att *att, uint8_t *enc_size) |
| { |
| struct bt_att_chan *chan; |
| int ret; |
| |
| if (!att) |
| return -EINVAL; |
| |
| chan = queue_peek_tail(att->chans); |
| if (!chan) |
| return -ENOTCONN; |
| |
| ret = bt_att_chan_get_security(chan); |
| if (ret < 0) |
| return ret; |
| |
| if (enc_size) |
| *enc_size = att->enc_size; |
| |
| return ret; |
| } |
| |
| bool bt_att_set_security(struct bt_att *att, int level) |
| { |
| struct bt_att_chan *chan; |
| |
| if (!att || level < BT_ATT_SECURITY_AUTO || |
| level > BT_ATT_SECURITY_HIGH) |
| return false; |
| |
| chan = queue_peek_tail(att->chans); |
| if (!chan) |
| return -ENOTCONN; |
| |
| return bt_att_chan_set_security(chan, level); |
| } |
| |
| void bt_att_set_enc_key_size(struct bt_att *att, uint8_t enc_size) |
| { |
| if (!att) |
| return; |
| |
| att->enc_size = enc_size; |
| } |
| |
| static bool sign_set_key(struct sign_info **sign, uint8_t key[16], |
| bt_att_counter_func_t func, void *user_data) |
| { |
| if (!(*sign)) |
| *sign = new0(struct sign_info, 1); |
| |
| (*sign)->counter = func; |
| (*sign)->user_data = user_data; |
| memcpy((*sign)->key, key, 16); |
| |
| return true; |
| } |
| |
| bool bt_att_set_local_key(struct bt_att *att, uint8_t sign_key[16], |
| bt_att_counter_func_t func, void *user_data) |
| { |
| if (!att) |
| return false; |
| |
| return sign_set_key(&att->local_sign, sign_key, func, user_data); |
| } |
| |
| bool bt_att_set_remote_key(struct bt_att *att, uint8_t sign_key[16], |
| bt_att_counter_func_t func, void *user_data) |
| { |
| if (!att) |
| return false; |
| |
| return sign_set_key(&att->remote_sign, sign_key, func, user_data); |
| } |
| |
| bool bt_att_has_crypto(struct bt_att *att) |
| { |
| if (!att) |
| return false; |
| |
| return att->crypto ? true : false; |
| } |
| |
| bool bt_att_set_retry(struct bt_att *att, unsigned int id, bool retry) |
| { |
| struct att_send_op *op; |
| |
| if (!id) |
| return false; |
| |
| op = queue_find(att->req_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_find(att->ind_queue, match_op_id, UINT_TO_PTR(id)); |
| if (op) |
| goto done; |
| |
| op = queue_find(att->write_queue, match_op_id, UINT_TO_PTR(id)); |
| |
| done: |
| if (!op) |
| return false; |
| |
| op->retry = !retry; |
| |
| return true; |
| } |