| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2012 Intel Corporation. All rights reserved. |
| * |
| * |
| * 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 <stdbool.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| |
| #include <glib.h> |
| #include <dbus/dbus.h> |
| #include <gdbus.h> |
| |
| #include <bluetooth/bluetooth.h> |
| #include <bluetooth/uuid.h> |
| #include <bluetooth/sdp.h> |
| #include <bluetooth/sdp_lib.h> |
| |
| #include "btio.h" |
| #include "sdpd.h" |
| #include "log.h" |
| #include "error.h" |
| #include "glib-helper.h" |
| #include "dbus-common.h" |
| #include "sdp-client.h" |
| #include "sdp-xml.h" |
| #include "adapter.h" |
| #include "manager.h" |
| #include "device.h" |
| #include "profile.h" |
| |
| #define SPP_DEFAULT_CHANNEL 3 |
| |
| struct ext_profile { |
| struct btd_profile p; |
| |
| char *name; |
| char *owner; |
| char *uuid; |
| char *path; |
| char *role; |
| char *record; |
| |
| char **remote_uuids; |
| |
| guint id; |
| |
| BtIOSecLevel sec_level; |
| bool authorize; |
| |
| bool enable_client; |
| bool enable_server; |
| |
| uint16_t psm; |
| uint8_t chan; |
| |
| GSList *servers; |
| GSList *conns; |
| |
| GSList *connects; |
| }; |
| |
| struct ext_io { |
| struct ext_profile *ext; |
| int proto; |
| GIOChannel *io; |
| guint io_id; |
| struct btd_adapter *adapter; |
| struct btd_device *device; |
| |
| bool resolving; |
| btd_profile_cb cb; |
| uint32_t rec_handle; |
| |
| uint16_t version; |
| uint16_t features; |
| |
| guint auth_id; |
| DBusPendingCall *new_conn; |
| }; |
| |
| struct btd_profile_custom_property { |
| char *uuid; |
| char *type; |
| char *name; |
| btd_profile_prop_exists exists; |
| btd_profile_prop_get get; |
| void *user_data; |
| }; |
| |
| static GSList *custom_props = NULL; |
| |
| static GSList *profiles = NULL; |
| static GSList *ext_profiles = NULL; |
| |
| void btd_profile_foreach(void (*func)(struct btd_profile *p, void *data), |
| void *data) |
| { |
| GSList *l, *next; |
| |
| for (l = profiles; l != NULL; l = next) { |
| struct btd_profile *profile = l->data; |
| |
| next = g_slist_next(l); |
| |
| func(profile, data); |
| } |
| |
| for (l = ext_profiles; l != NULL; l = next) { |
| struct ext_profile *profile = l->data; |
| |
| next = g_slist_next(l); |
| |
| func(&profile->p, data); |
| } |
| } |
| |
| int btd_profile_register(struct btd_profile *profile) |
| { |
| profiles = g_slist_append(profiles, profile); |
| return 0; |
| } |
| |
| void btd_profile_unregister(struct btd_profile *profile) |
| { |
| profiles = g_slist_remove(profiles, profile); |
| } |
| |
| static struct ext_profile *find_ext_profile(const char *owner, |
| const char *path) |
| { |
| GSList *l; |
| |
| for (l = ext_profiles; l != NULL; l = g_slist_next(l)) { |
| struct ext_profile *ext = l->data; |
| |
| if (!g_str_equal(ext->owner, owner)) |
| continue; |
| |
| if (g_str_equal(ext->path, path)) |
| return ext; |
| } |
| |
| return NULL; |
| } |
| |
| static void ext_cancel(struct ext_profile *ext) |
| { |
| DBusMessage *msg; |
| |
| msg = dbus_message_new_method_call(ext->owner, ext->path, |
| "org.bluez.Profile1", "Cancel"); |
| if (msg) |
| g_dbus_send_message(btd_get_dbus_connection(), msg); |
| } |
| |
| static void ext_io_destroy(gpointer p) |
| { |
| struct ext_io *ext_io = p; |
| struct ext_profile *ext = ext_io->ext; |
| |
| if (ext_io->io_id > 0) |
| g_source_remove(ext_io->io_id); |
| |
| if (ext_io->io) { |
| g_io_channel_shutdown(ext_io->io, FALSE, NULL); |
| g_io_channel_unref(ext_io->io); |
| } |
| |
| if (ext_io->auth_id != 0) |
| btd_cancel_authorization(ext_io->auth_id); |
| |
| if (ext_io->new_conn) { |
| dbus_pending_call_cancel(ext_io->new_conn); |
| dbus_pending_call_unref(ext_io->new_conn); |
| ext_cancel(ext); |
| } |
| |
| if (ext_io->resolving) |
| bt_cancel_discovery(adapter_get_address(ext_io->adapter), |
| device_get_address(ext_io->device)); |
| |
| if (ext_io->rec_handle) |
| remove_record_from_server(ext_io->rec_handle); |
| |
| if (ext_io->adapter) |
| btd_adapter_unref(ext_io->adapter); |
| |
| if (ext_io->device) |
| btd_device_unref(ext_io->device); |
| |
| g_free(ext_io); |
| } |
| |
| static gboolean ext_io_disconnected(GIOChannel *io, GIOCondition cond, |
| gpointer user_data) |
| { |
| struct ext_io *conn = user_data; |
| struct ext_profile *ext = conn->ext; |
| GError *gerr = NULL; |
| char addr[18]; |
| |
| if (cond & G_IO_NVAL) |
| return FALSE; |
| |
| bt_io_get(io, &gerr, BT_IO_OPT_DEST, addr, BT_IO_OPT_INVALID); |
| if (gerr != NULL) { |
| error("Unable to get io data for %s: %s", |
| ext->name, gerr->message); |
| g_error_free(gerr); |
| goto drop; |
| } |
| |
| DBG("%s disconnected from %s", ext->name, addr); |
| drop: |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| return FALSE; |
| } |
| |
| static void new_conn_reply(DBusPendingCall *call, void *user_data) |
| { |
| struct ext_io *conn = user_data; |
| struct ext_profile *ext = conn->ext; |
| DBusMessage *reply = dbus_pending_call_steal_reply(call); |
| DBusError err; |
| |
| dbus_error_init(&err); |
| dbus_set_error_from_message(&err, reply); |
| |
| dbus_message_unref(reply); |
| |
| dbus_pending_call_unref(conn->new_conn); |
| conn->new_conn = NULL; |
| |
| if (!dbus_error_is_set(&err)) { |
| if (conn->cb) { |
| conn->cb(&ext->p, conn->device, 0); |
| conn->cb = NULL; |
| } |
| return; |
| } |
| |
| error("%s replied with an error: %s, %s", ext->name, |
| err.name, err.message); |
| |
| if (conn->cb) { |
| conn->cb(&ext->p, conn->device, -ECONNREFUSED); |
| conn->cb = NULL; |
| } |
| |
| if (dbus_error_has_name(&err, DBUS_ERROR_NO_REPLY)) |
| ext_cancel(ext); |
| |
| dbus_error_free(&err); |
| |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| } |
| |
| struct prop_append_data { |
| DBusMessageIter *dict; |
| struct ext_io *io; |
| }; |
| |
| static void append_prop(gpointer a, gpointer b) |
| { |
| struct btd_profile_custom_property *p = a; |
| struct prop_append_data *data = b; |
| DBusMessageIter entry, value, *dict = data->dict; |
| struct btd_device *dev = data->io->device; |
| |
| if (p->exists && !p->exists(p->uuid, dev, p->user_data)) |
| return; |
| |
| dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, |
| &entry); |
| dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &p->name); |
| dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, p->type, |
| &value); |
| |
| p->get(p->uuid, dev, &value, p->user_data); |
| |
| dbus_message_iter_close_container(&entry, &value); |
| dbus_message_iter_close_container(dict, &entry); |
| } |
| |
| static bool send_new_connection(struct ext_profile *ext, struct ext_io *conn, |
| struct btd_device *dev) |
| { |
| DBusMessage *msg; |
| DBusMessageIter iter, dict; |
| struct prop_append_data data = { &dict, conn }; |
| const char *path; |
| int fd; |
| |
| msg = dbus_message_new_method_call(ext->owner, ext->path, |
| "org.bluez.Profile1", |
| "NewConnection"); |
| if (!msg) { |
| error("Unable to create NewConnection call for %s", ext->name); |
| return false; |
| } |
| |
| dbus_message_iter_init_append(msg, &iter); |
| |
| path = device_get_path(dev); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); |
| |
| fd = g_io_channel_unix_get_fd(conn->io); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_UNIX_FD, &fd); |
| |
| dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); |
| |
| if (conn->version) |
| dict_append_entry(&dict, "Version", DBUS_TYPE_UINT16, |
| &conn->version); |
| |
| if (conn->features) |
| dict_append_entry(&dict, "Features", DBUS_TYPE_UINT16, |
| &conn->features); |
| |
| g_slist_foreach(custom_props, append_prop, &data); |
| |
| dbus_message_iter_close_container(&iter, &dict); |
| |
| if (!dbus_connection_send_with_reply(btd_get_dbus_connection(), |
| msg, &conn->new_conn, -1)) { |
| error("%s: sending NewConnection failed", ext->name); |
| dbus_message_unref(msg); |
| return false; |
| } |
| |
| dbus_message_unref(msg); |
| |
| dbus_pending_call_set_notify(conn->new_conn, new_conn_reply, conn, |
| NULL); |
| |
| return true; |
| } |
| |
| static struct btd_device *get_btd_dev(bdaddr_t *src, const char *addr) |
| { |
| struct btd_adapter *adapter; |
| |
| adapter = manager_find_adapter(src); |
| if (!adapter) |
| return NULL; |
| |
| return adapter_get_device(adapter, addr); |
| } |
| |
| static void ext_connect(GIOChannel *io, GError *err, gpointer user_data) |
| { |
| struct ext_io *conn = user_data; |
| struct ext_profile *ext = conn->ext; |
| struct btd_device *device; |
| bdaddr_t src; |
| char addr[18]; |
| |
| if (!bt_io_get(io, NULL, |
| BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST, addr, |
| BT_IO_OPT_INVALID)) { |
| error("Unable to get connect data for %s", ext->name); |
| goto drop; |
| } |
| |
| if (err != NULL) { |
| error("%s failed to connect to %s: %s", ext->name, addr, |
| err->message); |
| goto drop; |
| } |
| |
| DBG("%s connected to %s", ext->name, addr); |
| |
| device = get_btd_dev(&src, addr); |
| if (!device) { |
| error("%s: Unable to get dev object for %s", ext->name, addr); |
| goto drop; |
| } |
| |
| if (conn->io_id == 0) { |
| GIOCondition cond = G_IO_HUP | G_IO_ERR | G_IO_NVAL; |
| conn->io_id = g_io_add_watch(io, cond, ext_io_disconnected, |
| conn); |
| } |
| |
| if (send_new_connection(ext, conn, device)) |
| return; |
| |
| drop: |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| } |
| |
| static void ext_auth(DBusError *err, void *user_data) |
| { |
| struct ext_io *conn = user_data; |
| struct ext_profile *ext = conn->ext; |
| GError *gerr = NULL; |
| char addr[18]; |
| |
| conn->auth_id = 0; |
| |
| bt_io_get(conn->io, &gerr, BT_IO_OPT_DEST, addr, BT_IO_OPT_INVALID); |
| if (gerr != NULL) { |
| error("Unable to get connect data for %s: %s", |
| ext->name, err->message); |
| g_error_free(gerr); |
| goto drop; |
| } |
| |
| if (err && dbus_error_is_set(err)) { |
| error("%s rejected %s: %s", ext->name, addr, err->message); |
| goto drop; |
| } |
| |
| if (!bt_io_accept(conn->io, ext_connect, conn, NULL, &gerr)) { |
| error("bt_io_accept: %s", gerr->message); |
| g_error_free(gerr); |
| goto drop; |
| } |
| |
| DBG("%s authorized to connect to %s", addr, ext->name); |
| |
| return; |
| |
| drop: |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| } |
| |
| static uint16_t get_supported_features(const sdp_record_t *rec) |
| { |
| sdp_data_t *data; |
| |
| data = sdp_data_get(rec, SDP_ATTR_SUPPORTED_FEATURES); |
| if (!data || data->dtd != SDP_UINT16) |
| return 0; |
| |
| return data->val.uint16; |
| } |
| |
| static uint16_t get_profile_version(const sdp_record_t *rec) |
| { |
| sdp_list_t *descs; |
| uint16_t version; |
| |
| if (sdp_get_profile_descs(rec, &descs) < 0) |
| return 0; |
| |
| if (descs && descs->data) { |
| sdp_profile_desc_t *desc = descs->data; |
| version = desc->version; |
| } else { |
| version = 0; |
| } |
| |
| sdp_list_free(descs, free); |
| |
| return version; |
| } |
| |
| static struct ext_io *create_conn(struct ext_io *server, GIOChannel *io, |
| bdaddr_t *src, bdaddr_t *dst) |
| { |
| struct btd_device *device; |
| char addr[18]; |
| struct ext_io *conn; |
| GIOCondition cond; |
| const char *remote_uuid = server->ext->remote_uuids[0]; |
| |
| conn = g_new0(struct ext_io, 1); |
| conn->io = g_io_channel_ref(io); |
| conn->proto = server->proto; |
| conn->ext = server->ext; |
| |
| ba2str(dst, addr); |
| device = adapter_find_device(server->adapter, addr); |
| |
| if (device && remote_uuid) { |
| const sdp_record_t *rec; |
| |
| rec = btd_device_get_record(device, remote_uuid); |
| if (rec) { |
| conn->features = get_supported_features(rec); |
| conn->version = get_profile_version(rec); |
| } |
| } |
| |
| cond = G_IO_HUP | G_IO_ERR | G_IO_NVAL; |
| conn->io_id = g_io_add_watch(io, cond, ext_io_disconnected, conn); |
| |
| return conn; |
| } |
| |
| static void ext_confirm(GIOChannel *io, gpointer user_data) |
| { |
| struct ext_io *server = user_data; |
| struct ext_profile *ext = server->ext; |
| struct ext_io *conn; |
| GError *gerr = NULL; |
| bdaddr_t src, dst; |
| char addr[18]; |
| |
| bt_io_get(io, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_DEST, addr, |
| BT_IO_OPT_INVALID); |
| if (gerr != NULL) { |
| error("%s failed to get connect data: %s", ext->name, |
| gerr->message); |
| g_error_free(gerr); |
| return; |
| } |
| |
| DBG("incoming connect from %s", addr); |
| |
| conn = create_conn(server, io, &src, &dst); |
| |
| conn->auth_id = btd_request_authorization(&src, &dst, ext->uuid, |
| ext_auth, conn); |
| if (conn->auth_id == 0) { |
| error("%s authorization failure", ext->name); |
| ext_io_destroy(conn); |
| return; |
| } |
| |
| ext->conns = g_slist_append(ext->conns, conn); |
| |
| DBG("%s authorizing connection from %s", ext->name, addr); |
| } |
| |
| static void ext_direct_connect(GIOChannel *io, GError *err, gpointer user_data) |
| { |
| struct ext_io *server = user_data; |
| struct ext_profile *ext = server->ext; |
| GError *gerr = NULL; |
| struct ext_io *conn; |
| bdaddr_t src, dst; |
| |
| bt_io_get(io, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, &src, |
| BT_IO_OPT_DEST_BDADDR, &dst, |
| BT_IO_OPT_INVALID); |
| if (gerr != NULL) { |
| error("%s failed to get connect data: %s", ext->name, |
| gerr->message); |
| g_error_free(gerr); |
| return; |
| } |
| |
| conn = create_conn(server, io, &src, &dst); |
| ext->conns = g_slist_append(ext->conns, conn); |
| |
| ext_connect(io, err, conn); |
| } |
| |
| static sdp_record_t *ext_get_record(struct ext_profile *ext) |
| { |
| return NULL; |
| } |
| |
| static uint32_t ext_register_record(struct ext_profile *ext, |
| const bdaddr_t *src) |
| { |
| sdp_record_t *rec; |
| |
| if (ext->record) |
| rec = sdp_xml_parse_record(ext->record, strlen(ext->record)); |
| else |
| rec = ext_get_record(ext); |
| |
| if (!rec) |
| return 0; |
| |
| if (add_record_to_server(src, rec) < 0) { |
| error("Failed to register service record"); |
| sdp_record_free(rec); |
| return 0; |
| } |
| |
| return rec->handle; |
| } |
| |
| static int ext_start_servers(struct ext_profile *ext, |
| struct btd_adapter *adapter) |
| { |
| struct ext_io *server; |
| BtIOConfirm confirm; |
| BtIOConnect connect; |
| GError *err = NULL; |
| uint32_t handle; |
| GIOChannel *io; |
| |
| handle = ext_register_record(ext, adapter_get_address(adapter)); |
| |
| if (ext->authorize) { |
| confirm = ext_confirm; |
| connect = NULL; |
| } else { |
| confirm = NULL; |
| connect = ext_direct_connect; |
| } |
| |
| if (ext->psm) { |
| server = g_new0(struct ext_io, 1); |
| server->ext = ext; |
| server->rec_handle = handle; |
| |
| io = bt_io_listen(connect, confirm, server, NULL, &err, |
| BT_IO_OPT_SOURCE_BDADDR, |
| adapter_get_address(adapter), |
| BT_IO_OPT_PSM, ext->psm, |
| BT_IO_OPT_SEC_LEVEL, ext->sec_level, |
| BT_IO_OPT_INVALID); |
| if (err != NULL) { |
| error("L2CAP server failed for %s: %s", |
| ext->name, err->message); |
| g_free(server); |
| g_clear_error(&err); |
| } else { |
| server->io = io; |
| server->proto = BTPROTO_L2CAP; |
| server->adapter = btd_adapter_ref(adapter); |
| ext->servers = g_slist_append(ext->servers, server); |
| DBG("%s listening on PSM %u", ext->name, ext->psm); |
| } |
| } |
| |
| if (ext->chan) { |
| server = g_new0(struct ext_io, 1); |
| server->ext = ext; |
| server->rec_handle = handle; |
| |
| io = bt_io_listen(connect, confirm, server, NULL, &err, |
| BT_IO_OPT_SOURCE_BDADDR, |
| adapter_get_address(adapter), |
| BT_IO_OPT_CHANNEL, ext->chan, |
| BT_IO_OPT_SEC_LEVEL, ext->sec_level, |
| BT_IO_OPT_INVALID); |
| if (err != NULL) { |
| error("RFCOMM server failed for %s: %s", |
| ext->name, err->message); |
| g_free(server); |
| g_clear_error(&err); |
| } else { |
| server->io = io; |
| server->proto = BTPROTO_RFCOMM; |
| server->adapter = btd_adapter_ref(adapter); |
| ext->servers = g_slist_append(ext->servers, server); |
| DBG("%s listening on chan %u", ext->name, ext->chan); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static struct ext_profile *find_ext(struct btd_profile *p) |
| { |
| GSList *l; |
| |
| l = g_slist_find(ext_profiles, p); |
| if (!l) |
| return NULL; |
| |
| return l->data; |
| } |
| |
| static int ext_adapter_probe(struct btd_profile *p, |
| struct btd_adapter *adapter) |
| { |
| struct ext_profile *ext; |
| |
| ext = find_ext(p); |
| if (!ext) |
| return -ENOENT; |
| |
| DBG("\"%s\" probed", ext->name); |
| |
| return ext_start_servers(ext, adapter); |
| } |
| |
| static void ext_adapter_remove(struct btd_profile *p, |
| struct btd_adapter *adapter) |
| { |
| struct ext_profile *ext; |
| GSList *l, *next; |
| |
| ext = find_ext(p); |
| if (!ext) |
| return; |
| |
| DBG("\"%s\" removed", ext->name); |
| |
| for (l = ext->servers; l != NULL; l = next) { |
| struct ext_io *server = l->data; |
| |
| next = g_slist_next(l); |
| |
| if (server->adapter != adapter) |
| continue; |
| |
| ext->servers = g_slist_remove(ext->servers, server); |
| ext_io_destroy(server); |
| } |
| } |
| |
| static int ext_device_probe(struct btd_profile *p, struct btd_device *dev, |
| GSList *uuids) |
| { |
| struct ext_profile *ext; |
| |
| ext = find_ext(p); |
| if (!ext) |
| return -ENOENT; |
| |
| DBG("%s probed with %u UUIDs", ext->name, g_slist_length(uuids)); |
| |
| return 0; |
| } |
| |
| static void remove_connect(struct ext_profile *ext, struct btd_device *dev) |
| { |
| GSList *l, *next; |
| |
| for (l = ext->conns; l != NULL; l = next) { |
| struct ext_io *conn = l->data; |
| |
| next = g_slist_next(l); |
| |
| if (!conn->cb) |
| continue; |
| |
| if (conn->device != dev) |
| continue; |
| |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| } |
| } |
| |
| static void ext_device_remove(struct btd_profile *p, struct btd_device *dev) |
| { |
| struct ext_profile *ext; |
| |
| ext = find_ext(p); |
| if (!ext) |
| return; |
| |
| DBG("%s", ext->name); |
| |
| remove_connect(ext, dev); |
| } |
| |
| static int connect_io(struct ext_io *conn, const bdaddr_t *src, |
| const bdaddr_t *dst) |
| { |
| struct ext_profile *ext = conn->ext; |
| GError *gerr = NULL; |
| GIOChannel *io; |
| |
| if (ext->psm) { |
| conn->proto = BTPROTO_L2CAP; |
| io = bt_io_connect(ext_connect, conn, NULL, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, src, |
| BT_IO_OPT_DEST_BDADDR, dst, |
| BT_IO_OPT_SEC_LEVEL, ext->sec_level, |
| BT_IO_OPT_PSM, ext->psm, |
| BT_IO_OPT_INVALID); |
| } else { |
| conn->proto = BTPROTO_RFCOMM; |
| io = bt_io_connect(ext_connect, conn, NULL, &gerr, |
| BT_IO_OPT_SOURCE_BDADDR, src, |
| BT_IO_OPT_DEST_BDADDR, dst, |
| BT_IO_OPT_SEC_LEVEL, ext->sec_level, |
| BT_IO_OPT_CHANNEL, ext->chan, |
| BT_IO_OPT_INVALID); |
| } |
| |
| if (gerr != NULL) { |
| error("Unable to connect %s: %s", ext->name, gerr->message); |
| g_error_free(gerr); |
| return -EIO; |
| } |
| |
| conn->io = io; |
| |
| return 0; |
| } |
| |
| static uint16_t get_goep_l2cap_psm(sdp_record_t *rec) |
| { |
| sdp_data_t *data; |
| |
| data = sdp_data_get(rec, SDP_ATTR_GOEP_L2CAP_PSM); |
| if (!data) |
| return 0; |
| |
| if (data->dtd != SDP_UINT16) |
| return 0; |
| |
| /* PSM must be odd and lsb of upper byte must be 0 */ |
| if ((data->val.uint16 & 0x0101) != 0x0001) |
| return 0; |
| |
| return data->val.uint16; |
| } |
| |
| static void record_cb(sdp_list_t *recs, int err, gpointer user_data) |
| { |
| struct ext_io *conn = user_data; |
| struct ext_profile *ext = conn->ext; |
| sdp_list_t *r; |
| |
| conn->resolving = false; |
| |
| if (err < 0) { |
| error("Unable to get %s SDP record: %s", ext->name, |
| strerror(-err)); |
| goto failed; |
| } |
| |
| if (!recs || !recs->data) { |
| error("No SDP records found for %s", ext->name); |
| goto failed; |
| } |
| |
| for (r = recs; r != NULL; r = r->next) { |
| sdp_record_t *rec = r->data; |
| sdp_list_t *protos; |
| int port; |
| |
| if (sdp_get_access_protos(rec, &protos) < 0) { |
| error("Unable to get proto list from %s record", |
| ext->name); |
| goto failed; |
| } |
| |
| port = sdp_get_proto_port(protos, L2CAP_UUID); |
| if (port > 0) |
| ext->psm = port; |
| |
| port = sdp_get_proto_port(protos, RFCOMM_UUID); |
| if (port > 0) |
| ext->chan = port; |
| |
| if (ext->psm == 0 && sdp_get_proto_desc(protos, OBEX_UUID)) |
| ext->psm = get_goep_l2cap_psm(rec); |
| |
| conn->features = get_supported_features(rec); |
| conn->version = get_profile_version(rec); |
| |
| sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, |
| NULL); |
| sdp_list_free(protos, NULL); |
| |
| if (ext->chan || ext->psm) |
| break; |
| } |
| |
| if (!ext->chan && !ext->psm) { |
| error("Failed to find L2CAP PSM or RFCOMM channel for %s", |
| ext->name); |
| goto failed; |
| } |
| |
| err = connect_io(conn, adapter_get_address(conn->adapter), |
| device_get_address(conn->device)); |
| if (err < 0) { |
| error("Connecting %s failed: %s", ext->name, strerror(-err)); |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| conn->cb(&ext->p, conn->device, err); |
| ext->conns = g_slist_remove(ext->conns, conn); |
| ext_io_destroy(conn); |
| } |
| |
| static int resolve_service(struct ext_io *conn, const bdaddr_t *src, |
| const bdaddr_t *dst) |
| { |
| struct ext_profile *ext = conn->ext; |
| uuid_t uuid; |
| int err; |
| |
| bt_string2uuid(&uuid, ext->remote_uuids[0]); |
| |
| err = bt_search_service(src, dst, &uuid, record_cb, conn, NULL); |
| if (err == 0) |
| conn->resolving = true; |
| |
| return err; |
| } |
| |
| static int ext_connect_dev(struct btd_device *dev, struct btd_profile *profile, |
| btd_profile_cb cb) |
| { |
| struct btd_adapter *adapter; |
| struct ext_io *conn; |
| struct ext_profile *ext; |
| int err; |
| |
| ext = find_ext(profile); |
| if (!ext) |
| return -ENOENT; |
| |
| adapter = device_get_adapter(dev); |
| |
| conn = g_new0(struct ext_io, 1); |
| conn->ext = ext; |
| |
| if (ext->psm || ext->chan) |
| err = connect_io(conn, adapter_get_address(adapter), |
| device_get_address(dev)); |
| else |
| err = resolve_service(conn, adapter_get_address(adapter), |
| device_get_address(dev)); |
| |
| if (err < 0) |
| goto failed; |
| |
| conn->adapter = btd_adapter_ref(adapter); |
| conn->device = btd_device_ref(dev); |
| conn->cb = cb; |
| |
| ext->conns = g_slist_append(ext->conns, conn); |
| |
| return 0; |
| |
| failed: |
| g_free(conn); |
| return err; |
| } |
| |
| static int ext_disconnect_dev(struct btd_device *dev, |
| struct btd_profile *profile, |
| btd_profile_cb cb) |
| { |
| struct ext_profile *ext; |
| |
| ext = find_ext(profile); |
| if (!ext) |
| return -ENOENT; |
| |
| remove_connect(ext, dev); |
| |
| return 0; |
| } |
| |
| static void ext_get_defaults(struct ext_profile *ext) |
| { |
| if (ext->enable_client && !ext->remote_uuids[0]) { |
| const char *remote_uuid; |
| |
| g_strfreev(ext->remote_uuids); |
| ext->remote_uuids = g_new0(char *, 2); |
| |
| if (strcasecmp(ext->uuid, HFP_HS_UUID) == 0) |
| remote_uuid = HFP_AG_UUID; |
| else if (strcasecmp(ext->uuid, HFP_AG_UUID) == 0) |
| remote_uuid = HFP_HS_UUID; |
| else if (strcasecmp(ext->uuid, HSP_HS_UUID) == 0) |
| remote_uuid = HSP_AG_UUID; |
| else |
| remote_uuid = ext->uuid; |
| |
| ext->remote_uuids[0] = g_strdup(remote_uuid); |
| } |
| |
| if (strcasecmp(ext->uuid, SPP_UUID) == 0) { |
| if (ext->enable_server && !ext->chan) |
| ext->chan = SPP_DEFAULT_CHANNEL; |
| } |
| } |
| |
| static int parse_ext_opt(struct ext_profile *ext, const char *key, |
| DBusMessageIter *value) |
| { |
| int type = dbus_message_iter_get_arg_type(value); |
| const char *str; |
| |
| if (strcasecmp(key, "Name") == 0) { |
| if (type != DBUS_TYPE_STRING) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &str); |
| g_free(ext->name); |
| ext->name = g_strdup(str); |
| } else if (strcasecmp(key, "AutoConnect") == 0) { |
| if (type != DBUS_TYPE_BOOLEAN) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &ext->p.auto_connect); |
| } else if (strcasecmp(key, "PSM") == 0) { |
| if (type != DBUS_TYPE_UINT16) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &ext->psm); |
| } else if (strcasecmp(key, "Channel") == 0) { |
| uint16_t ch; |
| |
| if (type != DBUS_TYPE_UINT16) |
| return -EINVAL; |
| |
| dbus_message_iter_get_basic(value, &ch); |
| ext->chan = ch; |
| } else if (strcasecmp(key, "RequireAuthentication") == 0) { |
| dbus_bool_t b; |
| |
| if (type != DBUS_TYPE_BOOLEAN) |
| return -EINVAL; |
| |
| dbus_message_iter_get_basic(value, &b); |
| if (b) |
| ext->sec_level = BT_IO_SEC_MEDIUM; |
| else |
| ext->sec_level = BT_IO_SEC_LOW; |
| } else if (strcasecmp(key, "RequireAuthorization") == 0) { |
| if (type != DBUS_TYPE_BOOLEAN) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &ext->authorize); |
| } else if (strcasecmp(key, "Role") == 0) { |
| if (type != DBUS_TYPE_STRING) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &str); |
| g_free(ext->role); |
| ext->role = g_strdup(str); |
| |
| if (g_str_equal(ext->role, "client")) { |
| ext->enable_server = false; |
| ext->enable_client = true; |
| } else if (g_str_equal(ext->role, "server")) { |
| ext->enable_server = true; |
| ext->enable_client = false; |
| } |
| } else if (strcasecmp(key, "ServiceRecord") == 0) { |
| if (type != DBUS_TYPE_STRING) |
| return -EINVAL; |
| dbus_message_iter_get_basic(value, &str); |
| g_free(ext->record); |
| ext->record = g_strdup(str); |
| ext->enable_server = true; |
| } |
| |
| return 0; |
| } |
| |
| static struct ext_profile *create_ext(const char *owner, const char *path, |
| const char *uuid, |
| DBusMessageIter *opts) |
| { |
| struct btd_profile *p; |
| struct ext_profile *ext; |
| |
| ext = g_new0(struct ext_profile, 1); |
| |
| ext->owner = g_strdup(owner); |
| ext->path = g_strdup(path); |
| |
| ext->uuid = bt_name2string(uuid); |
| |
| ext->remote_uuids = g_new0(char *, 1); |
| |
| ext->sec_level = BT_IO_SEC_LOW; |
| ext->enable_client = true; |
| ext->enable_server = true; |
| |
| while (dbus_message_iter_get_arg_type(opts) == DBUS_TYPE_DICT_ENTRY) { |
| DBusMessageIter value, entry; |
| const char *key; |
| |
| dbus_message_iter_recurse(opts, &entry); |
| dbus_message_iter_get_basic(&entry, &key); |
| |
| dbus_message_iter_next(&entry); |
| dbus_message_iter_recurse(&entry, &value); |
| |
| if (parse_ext_opt(ext, key, &value) < 0) |
| error("Invalid value for profile option %s", key); |
| |
| dbus_message_iter_next(opts); |
| } |
| |
| if (!ext->name) |
| ext->name = g_strdup_printf("%s%s/%s", owner, path, uuid); |
| |
| ext_get_defaults(ext); |
| |
| p = &ext->p; |
| |
| p->name = ext->name; |
| p->local_uuid = ext->uuid; |
| |
| /* Typecast can't really be avoided here: |
| * http://c-faq.com/ansi/constmismatch.html */ |
| p->remote_uuids = (const char **) ext->remote_uuids; |
| |
| if (ext->enable_server) { |
| p->adapter_probe = ext_adapter_probe; |
| p->adapter_remove = ext_adapter_remove; |
| } |
| |
| if (ext->enable_client) { |
| p->device_probe = ext_device_probe; |
| p->device_remove = ext_device_remove; |
| p->connect = ext_connect_dev; |
| p->disconnect = ext_disconnect_dev; |
| } |
| |
| DBG("Created \"%s\"", ext->name); |
| |
| ext_profiles = g_slist_append(ext_profiles, ext); |
| |
| manager_foreach_adapter(adapter_add_profile, &ext->p); |
| |
| return ext; |
| } |
| |
| static void remove_ext(struct ext_profile *ext) |
| { |
| manager_foreach_adapter(adapter_remove_profile, &ext->p); |
| |
| ext_profiles = g_slist_remove(ext_profiles, ext); |
| |
| DBG("Removed \"%s\"", ext->name); |
| |
| g_slist_free_full(ext->servers, ext_io_destroy); |
| g_slist_free_full(ext->conns, ext_io_destroy); |
| |
| g_strfreev(ext->remote_uuids); |
| |
| g_free(ext->name); |
| g_free(ext->owner); |
| g_free(ext->uuid); |
| g_free(ext->role); |
| g_free(ext->path); |
| g_free(ext->record); |
| |
| g_free(ext); |
| } |
| |
| static void ext_exited(DBusConnection *conn, void *user_data) |
| { |
| struct ext_profile *ext = user_data; |
| |
| DBG("\"%s\" exited", ext->name); |
| |
| remove_ext(ext); |
| } |
| |
| static DBusMessage *register_profile(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| const char *path, *sender, *uuid; |
| DBusMessageIter args, opts; |
| struct ext_profile *ext; |
| |
| sender = dbus_message_get_sender(msg); |
| |
| dbus_message_iter_init(msg, &args); |
| |
| dbus_message_iter_get_basic(&args, &path); |
| dbus_message_iter_next(&args); |
| |
| ext = find_ext_profile(sender, path); |
| if (ext) |
| return btd_error_already_exists(msg); |
| |
| dbus_message_iter_get_basic(&args, &uuid); |
| dbus_message_iter_next(&args); |
| |
| dbus_message_iter_recurse(&args, &opts); |
| if (dbus_message_iter_get_arg_type(&opts) != DBUS_TYPE_DICT_ENTRY) |
| return btd_error_invalid_args(msg); |
| |
| ext = create_ext(sender, path, uuid, &opts); |
| if (!ext) |
| return btd_error_invalid_args(msg); |
| |
| ext->id = g_dbus_add_disconnect_watch(conn, sender, ext_exited, ext, |
| NULL); |
| |
| return dbus_message_new_method_return(msg); |
| } |
| |
| static DBusMessage *unregister_profile(DBusConnection *conn, |
| DBusMessage *msg, void *user_data) |
| { |
| const char *path, *sender; |
| struct ext_profile *ext; |
| |
| sender = dbus_message_get_sender(msg); |
| |
| if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path, |
| DBUS_TYPE_INVALID)) |
| return btd_error_invalid_args(msg); |
| |
| ext = find_ext_profile(sender, path); |
| if (!ext) |
| return btd_error_does_not_exist(msg); |
| |
| g_dbus_remove_watch(conn, ext->id); |
| remove_ext(ext); |
| |
| return dbus_message_new_method_return(msg); |
| } |
| |
| static const GDBusMethodTable methods[] = { |
| { GDBUS_METHOD("RegisterProfile", |
| GDBUS_ARGS({ "profile", "o"}, { "UUID", "s" }, |
| { "options", "a{sv}" }), |
| NULL, register_profile) }, |
| { GDBUS_METHOD("UnregisterProfile", GDBUS_ARGS({ "profile", "o" }), |
| NULL, unregister_profile) }, |
| { } |
| }; |
| |
| void btd_profile_add_custom_prop(const char *uuid, const char *type, |
| const char *name, |
| btd_profile_prop_exists exists, |
| btd_profile_prop_get get, |
| void *user_data) |
| { |
| struct btd_profile_custom_property *prop; |
| |
| prop = g_new0(struct btd_profile_custom_property, 1); |
| |
| prop->uuid = g_strdup(uuid); |
| prop->type = g_strdup(type); |
| prop->name = g_strdup(name); |
| prop->exists = exists; |
| prop->get = get; |
| prop->user_data = user_data; |
| |
| custom_props = g_slist_append(custom_props, prop); |
| } |
| |
| static void free_property(gpointer data) |
| { |
| struct btd_profile_custom_property *p = data; |
| |
| g_free(p->uuid); |
| g_free(p->type); |
| g_free(p->name); |
| |
| g_free(p); |
| } |
| |
| void btd_profile_init(void) |
| { |
| g_dbus_register_interface(btd_get_dbus_connection(), |
| "/org/bluez", "org.bluez.ProfileManager1", |
| methods, NULL, NULL, NULL, NULL); |
| } |
| |
| void btd_profile_cleanup(void) |
| { |
| while (ext_profiles) { |
| struct ext_profile *ext = ext_profiles->data; |
| DBusConnection *conn = btd_get_dbus_connection(); |
| DBusMessage *msg; |
| |
| DBG("Releasing \"%s\"", ext->name); |
| |
| g_slist_free_full(ext->conns, ext_io_destroy); |
| ext->conns = NULL; |
| |
| msg = dbus_message_new_method_call(ext->owner, ext->path, |
| "org.bluez.Profile1", |
| "Release"); |
| if (msg) |
| g_dbus_send_message(conn, msg); |
| |
| g_dbus_remove_watch(conn, ext->id); |
| remove_ext(ext); |
| |
| } |
| |
| g_slist_free_full(custom_props, free_property); |
| custom_props = NULL; |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), |
| "/org/bluez", "org.bluez.ProfileManager1"); |
| |
| } |