diff options
Diffstat (limited to 'input')
-rw-r--r-- | input/Makefile.am | 24 | ||||
-rw-r--r-- | input/device.c | 1171 | ||||
-rw-r--r-- | input/device.h | 50 | ||||
-rw-r--r-- | input/fakehid.c | 412 | ||||
-rw-r--r-- | input/fakehid.h | 39 | ||||
-rw-r--r-- | input/input-api.txt | 103 | ||||
-rw-r--r-- | input/input.conf | 9 | ||||
-rw-r--r-- | input/main.c | 154 | ||||
-rw-r--r-- | input/manager.c | 801 | ||||
-rw-r--r-- | input/manager.h | 28 | ||||
-rw-r--r-- | input/server.c | 153 | ||||
-rw-r--r-- | input/server.h | 25 | ||||
-rw-r--r-- | input/storage.c | 364 | ||||
-rw-r--r-- | input/storage.h | 41 | ||||
-rwxr-xr-x | input/test-input | 45 |
15 files changed, 3419 insertions, 0 deletions
diff --git a/input/Makefile.am b/input/Makefile.am new file mode 100644 index 00000000..1bc81fbe --- /dev/null +++ b/input/Makefile.am @@ -0,0 +1,24 @@ + +if INPUTPLUGIN +plugindir = $(libdir)/bluetooth/plugins + +plugin_LTLIBRARIES = input.la + +input_la_SOURCES = main.c manager.h manager.c \ + server.h server.c device.h device.c \ + storage.h storage.c fakehid.c fakehid.h + +LDADD = $(top_builddir)/common/libhelper.a \ + @GDBUS_LIBS@ @GLIB_LIBS@ @DBUS_LIBS@ @BLUEZ_LIBS@ +endif + +AM_LDFLAGS = -module -avoid-version -no-undefined \ + -export-symbols-regex bluetooth_plugin_desc + +AM_CFLAGS = @BLUEZ_CFLAGS@ @DBUS_CFLAGS@ @GLIB_CFLAGS@ @GDBUS_CFLAGS@ + +INCLUDES = -I$(top_srcdir)/common -I$(top_srcdir)/hcid + +EXTRA_DIST = input.conf input-api.txt test-input + +MAINTAINERCLEANFILES = Makefile.in diff --git a/input/device.c b/input/device.c new file mode 100644 index 00000000..75acaa82 --- /dev/null +++ b/input/device.c @@ -0,0 +1,1171 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <stdlib.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/hidp.h> +#include <bluetooth/l2cap.h> +#include <bluetooth/rfcomm.h> +#include <bluetooth/sdp.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "logging.h" +#include "textfile.h" +#include "uinput.h" + +#include "device.h" +#include "error.h" +#include "manager.h" +#include "storage.h" +#include "fakehid.h" +#include "glib-helper.h" + +#define INPUT_DEVICE_INTERFACE "org.bluez.input.Device" + +#define BUF_SIZE 16 + +#define UPDOWN_ENABLED 1 + +#define FI_FLAG_CONNECTED 1 + +struct device; + +struct device { + bdaddr_t src; + bdaddr_t dst; + int timeout; + char *name; + uint8_t major; + uint8_t minor; + uint16_t product; + uint16_t vendor; + struct fake_input *fake; + DBusMessage *pending_connect; + DBusConnection *conn; + char *path; + int ctrl_sk; + int intr_sk; + guint ctrl_watch; + guint intr_watch; +}; + +GSList *devices = NULL; + +static struct device *device_new(bdaddr_t *src, bdaddr_t *dst, + uint8_t subclass, int timeout) +{ + struct device *idev; + uint32_t cls; + uint8_t major, minor; + + if (!subclass) { + if (read_device_class(src, dst, &cls) < 0) + return NULL; + + major = (cls >> 8) & 0x1f; + minor = (cls >> 2) & 0x3f; + } else { + major = 0x05; /* Peripheral */ + minor = (subclass >> 2) & 0x3f; + } + + idev = g_new0(struct device, 1); + + bacpy(&idev->src, src); + bacpy(&idev->dst, dst); + idev->timeout = timeout; + + read_device_name(src, dst, &idev->name); + + idev->major = major; + idev->minor = minor; + idev->ctrl_sk = -1; + idev->intr_sk = -1; + + return idev; +} + +static void device_free(struct device *idev) +{ + if (!idev) + return; + if (idev->name) + g_free(idev->name); + if (idev->fake) + g_free(idev->fake); + if (idev->path) + g_free(idev->path); + if (idev->pending_connect) + dbus_message_unref(idev->pending_connect); + dbus_connection_unref(idev->conn); + g_free(idev); +} + +static int uinput_create(char *name) +{ + struct uinput_dev dev; + int fd, err; + + fd = open("/dev/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/input/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/misc/uinput", O_RDWR); + if (fd < 0) { + err = errno; + error("Can't open input device: %s (%d)", + strerror(err), err); + return -err; + } + } + } + + memset(&dev, 0, sizeof(dev)); + if (name) + strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE); + + dev.id.bustype = BUS_BLUETOOTH; + dev.id.vendor = 0x0000; + dev.id.product = 0x0000; + dev.id.version = 0x0000; + + if (write(fd, &dev, sizeof(dev)) < 0) { + err = errno; + error("Can't write device information: %s (%d)", + strerror(err), err); + close(fd); + errno = err; + return -err; + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_EVBIT, EV_REP); + + ioctl(fd, UI_SET_KEYBIT, KEY_UP); + ioctl(fd, UI_SET_KEYBIT, KEY_PAGEUP); + ioctl(fd, UI_SET_KEYBIT, KEY_DOWN); + ioctl(fd, UI_SET_KEYBIT, KEY_PAGEDOWN); + + if (ioctl(fd, UI_DEV_CREATE, NULL) < 0) { + err = errno; + error("Can't create uinput device: %s (%d)", + strerror(err), err); + close(fd); + errno = err; + return -err; + } + + return fd; +} + +static const char *create_input_path(uint8_t major, uint8_t minor) +{ + static char path[48]; + char subpath[32]; + static int next_id = 0; + + switch (major) { + case 0x02: /* Phone */ + strcpy(subpath, "phone"); + break; + case 0x04: /* Audio */ + switch (minor) { + /* FIXME: Testing required */ + case 0x01: /* Wearable Headset Device */ + strcpy(subpath, "wearable"); + break; + case 0x02: /* Hands-free */ + strcpy(subpath, "handsfree"); + break; + case 0x06: /* Headphone */ + strcpy(subpath, "headphone"); + break; + default: + return NULL; + } + break; + case 0x05: /* Peripheral */ + switch (minor & 0x30) { + case 0x10: + strcpy(subpath, "keyboard"); + break; + case 0x20: + strcpy(subpath, "pointing"); + break; + case 0x30: + strcpy(subpath, "combo"); + break; + default: + subpath[0] = '\0'; + break; + } + + if ((minor & 0x0f) && (strlen(subpath) > 0)) + strcat(subpath, "/"); + + switch (minor & 0x0f) { + case 0x00: + break; + case 0x01: + strcat(subpath, "joystick"); + break; + case 0x02: + strcat(subpath, "gamepad"); + break; + case 0x03: + strcat(subpath, "remotecontrol"); + break; + case 0x04: + strcat(subpath, "sensing"); + break; + case 0x05: + strcat(subpath, "digitizertablet"); + break; + case 0x06: + strcat(subpath, "cardreader"); + break; + default: + strcat(subpath, "reserved"); + break; + } + break; + default: + return NULL; + } + + snprintf(path, 48, "%s/%s%d", INPUT_PATH, subpath, next_id++); + return path; +} + +static int decode_key(const char *str) +{ + static int mode = UPDOWN_ENABLED, gain = 0; + + uint16_t key; + int new_gain; + + /* Switch from key up/down to page up/down */ + if (strncmp("AT+CKPD=200", str, 11) == 0) { + mode = ~mode; + return KEY_RESERVED; + } + + if (strncmp("AT+VG", str, 5)) + return KEY_RESERVED; + + /* Gain key pressed */ + if (strlen(str) != 10) + return KEY_RESERVED; + + new_gain = strtol(&str[7], NULL, 10); + if (new_gain <= gain) + key = (mode == UPDOWN_ENABLED ? KEY_UP : KEY_PAGEUP); + else + key = (mode == UPDOWN_ENABLED ? KEY_DOWN : KEY_PAGEDOWN); + + gain = new_gain; + + return key; +} + +static void send_event(int fd, uint16_t type, uint16_t code, int32_t value) +{ + struct uinput_event event; + int err; + + memset(&event, 0, sizeof(event)); + event.type = type; + event.code = code; + event.value = value; + + err = write(fd, &event, sizeof(event)); +} + +static void send_key(int fd, uint16_t key) +{ + /* Key press */ + send_event(fd, EV_KEY, key, 1); + send_event(fd, EV_SYN, SYN_REPORT, 0); + /* Key release */ + send_event(fd, EV_KEY, key, 0); + send_event(fd, EV_SYN, SYN_REPORT, 0); +} + +static gboolean rfcomm_io_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + struct fake_input *fake = data; + const char *ok = "\r\nOK\r\n"; + char buf[BUF_SIZE]; + gsize bread = 0, bwritten; + uint16_t key; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + error("Hangup or error on rfcomm server socket"); + goto failed; + } + + memset(buf, 0, BUF_SIZE); + if (g_io_channel_read(chan, buf, sizeof(buf) - 1, + &bread) != G_IO_ERROR_NONE) { + error("IO Channel read error"); + goto failed; + } + + debug("Received: %s", buf); + + if (g_io_channel_write(chan, ok, 6, &bwritten) != G_IO_ERROR_NONE) { + error("IO Channel write error"); + goto failed; + } + + key = decode_key(buf); + if (key != KEY_RESERVED) + send_key(fake->uinput, key); + + return TRUE; + +failed: + ioctl(fake->uinput, UI_DEV_DESTROY); + close(fake->uinput); + fake->uinput = -1; + g_io_channel_unref(fake->io); + + return FALSE; +} + +static inline DBusMessage *in_progress(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".InProgress", + "Device connection already in progress"); +} + +static inline DBusMessage *already_connected(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".AlreadyConnected", + "Already connected to a device"); +} + +static inline DBusMessage *connection_attempt_failed(DBusMessage *msg, int err) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".ConnectionAttemptFailed", + err ? strerror(err) : "Connection attempt failed"); +} + +static void rfcomm_connect_cb(GIOChannel *chan, int err, const bdaddr_t *src, + const bdaddr_t *dst, gpointer user_data) +{ + struct device *idev = user_data; + struct fake_input *fake; + DBusMessage *reply; + const char *path; + + fake = idev->fake; + fake->rfcomm = g_io_channel_unix_get_fd(chan); + + if (err < 0) + goto failed; + + /* + * FIXME: Some headsets required a sco connection + * first to report volume gain key events + */ + fake->uinput = uinput_create(idev->name); + if (fake->uinput < 0) { + err = errno; + goto failed; + } + + fake->io = g_io_channel_unix_new(fake->rfcomm); + g_io_channel_set_close_on_unref(fake->io, TRUE); + g_io_add_watch(fake->io, G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) rfcomm_io_cb, fake); + + /* Replying to the requestor */ + reply = dbus_message_new_method_return(idev->pending_connect); + g_dbus_send_message(idev->conn, reply); + + /* Sending the Connected signal */ + path = dbus_message_get_path(idev->pending_connect); + g_dbus_emit_signal(idev->conn, path, + INPUT_DEVICE_INTERFACE, "Connected", + DBUS_TYPE_INVALID); + + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; + + return; + +failed: + reply = connection_attempt_failed(idev->pending_connect, err); + g_dbus_send_message(idev->conn, reply); + + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; +} + +static int rfcomm_connect(struct device *idev) +{ + int err; + + err = bt_rfcomm_connect(&idev->src, &idev->dst, idev->fake->ch, + rfcomm_connect_cb, idev); + if (err < 0) { + error("connect() failed: %s (%d)", strerror(-err), -err); + return err; + } + + return 0; +} + +static gboolean intr_watch_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + struct device *idev = data; + + if (cond & (G_IO_HUP | G_IO_ERR)) + g_io_channel_close(chan); + + g_dbus_emit_signal(idev->conn, + idev->path, + INPUT_DEVICE_INTERFACE, + "Disconnected", + DBUS_TYPE_INVALID); + + g_source_remove(idev->ctrl_watch); + idev->ctrl_watch = 0; + idev->intr_watch = 0; + + /* Close control channel */ + if (idev->ctrl_sk > 0) { + close(idev->ctrl_sk); + idev->ctrl_sk = -1; + } + + return FALSE; + +} + +static gboolean ctrl_watch_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + struct device *idev = data; + + if (cond & (G_IO_HUP | G_IO_ERR)) + g_io_channel_close(chan); + + g_dbus_emit_signal(idev->conn, + idev->path, + INPUT_DEVICE_INTERFACE, + "Disconnected", + DBUS_TYPE_INVALID); + + g_source_remove(idev->intr_watch); + idev->intr_watch = 0; + idev->ctrl_watch = 0; + + /* Close interrupt channel */ + if (idev->intr_sk > 0) { + close(idev->intr_sk); + idev->intr_sk = -1; + } + + return FALSE; +} + +static guint create_watch(int sk, GIOFunc cb, struct device *idev) +{ + guint id; + GIOChannel *io; + + io = g_io_channel_unix_new(sk); + id = g_io_add_watch(io, G_IO_HUP | G_IO_ERR | G_IO_NVAL, cb, idev); + g_io_channel_unref(io); + + return id; +} + +static int hidp_connadd(bdaddr_t *src, bdaddr_t *dst, + int ctrl_sk, int intr_sk, int timeout, const char *name) +{ + struct hidp_connadd_req req; + char addr[18]; + int ctl, err; + + ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); + if (ctl < 0) { + error("Can't open HIDP interface"); + return -errno; + } + + ba2str(dst, addr); + + memset(&req, 0, sizeof(req)); + req.ctrl_sock = ctrl_sk; + req.intr_sock = intr_sk; + req.flags = 0; + req.idle_to = timeout; + + err = get_stored_device_info(src, dst, &req); + if (err < 0) { + error("Rejected connection from unknown device %s", addr); + goto cleanup; + } + + if (req.subclass & 0x40) { + err = encrypt_link(src, dst); + if (err < 0 && err != -EACCES) + goto cleanup; + } + + if (name) + strncpy(req.name, name, 128); + + info("New input device %s (%s)", addr, req.name); + + if (req.vendor == 0x054c && req.product == 0x0268) { + unsigned char buf[] = { 0x53, 0xf4, 0x42, 0x03, 0x00, 0x00 }; + err = write(ctrl_sk, buf, sizeof(buf)); + } + + err = ioctl(ctl, HIDPCONNADD, &req); + +cleanup: + close(ctl); + + if (req.rd_data) + free(req.rd_data); + + return err; +} + +static void interrupt_connect_cb(GIOChannel *chan, int err, const bdaddr_t *src, + const bdaddr_t *dst, gpointer user_data) +{ + struct device *idev = user_data; + DBusMessage *reply; + + if (err < 0) { + error("connect(): %s (%d)", strerror(-err), -err); + goto failed; + } + + idev->intr_sk = g_io_channel_unix_get_fd(chan); + err = hidp_connadd(&idev->src, &idev->dst, + idev->ctrl_sk, idev->intr_sk, + idev->timeout, idev->name); + if (err < 0) + goto failed; + + idev->intr_watch = create_watch(idev->intr_sk, intr_watch_cb, idev); + idev->ctrl_watch = create_watch(idev->ctrl_sk, ctrl_watch_cb, idev); + g_dbus_emit_signal(idev->conn, + idev->path, + INPUT_DEVICE_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + + /* Replying to the requestor */ + g_dbus_send_reply(idev->conn, idev->pending_connect, DBUS_TYPE_INVALID); + + goto cleanup; + +failed: + reply = connection_attempt_failed(idev->pending_connect, -err); + g_dbus_send_message(idev->conn, reply); + + idev->intr_sk = -1; + idev->ctrl_sk = -1; + +cleanup: + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; +} + +static void control_connect_cb(GIOChannel *chan, int err, const bdaddr_t *src, + const bdaddr_t *dst, gpointer user_data) +{ + struct device *idev = user_data; + + if (err < 0) { + error("connect(): %s (%d)", strerror(-err), -err); + goto failed; + } + + /* Set HID control channel */ + idev->ctrl_sk = g_io_channel_unix_get_fd(chan); + + /* Connect to the HID interrupt channel */ + err = bt_l2cap_connect(&idev->src, &idev->dst, L2CAP_PSM_HIDP_INTR, 0, + interrupt_connect_cb, idev); + if (err < 0) { + error("L2CAP connect failed:%s (%d)", strerror(-err), -err); + goto failed; + } + + return; + +failed: + idev->ctrl_sk = -1; + error_connection_attempt_failed(idev->conn, + idev->pending_connect, -err); + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; +} + +static int fake_disconnect(struct device *idev) +{ + struct fake_input *fake = idev->fake; + + if (!fake->io) + return -ENOTCONN; + + g_io_channel_close(fake->io); + g_io_channel_unref(fake->io); + fake->io = NULL; + + if (fake->uinput >= 0) { + ioctl(fake->uinput, UI_DEV_DESTROY); + close(fake->uinput); + fake->uinput = -1; + } + + return 0; +} + +static int disconnect(struct device *idev, uint32_t flags) +{ + struct fake_input *fake = idev->fake; + struct hidp_conndel_req req; + struct hidp_conninfo ci; + int ctl, err; + + /* Fake input disconnect */ + if (fake) { + err = fake->disconnect(idev); + if (err == 0) + fake->flags &= ~FI_FLAG_CONNECTED; + return err; + } + + /* Standard HID disconnect */ + if (idev->ctrl_sk >= 0) { + close(idev->ctrl_sk); + idev->ctrl_sk = -1; + } + if (idev->intr_sk >= 0) { + close(idev->intr_sk); + idev->intr_sk = -1; + } + + ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); + if (ctl < 0) { + error("Can't open HIDP control socket"); + return -errno; + } + + memset(&ci, 0, sizeof(ci)); + bacpy(&ci.bdaddr, &idev->dst); + if ((ioctl(ctl, HIDPGETCONNINFO, &ci) < 0) || + (ci.state != BT_CONNECTED)) { + errno = ENOTCONN; + goto fail; + } + + memset(&req, 0, sizeof(req)); + bacpy(&req.bdaddr, &idev->dst); + req.flags = flags; + if (ioctl(ctl, HIDPCONNDEL, &req) < 0) { + error("Can't delete the HID device: %s(%d)", + strerror(errno), errno); + goto fail; + } + + close(ctl); + + return 0; + +fail: + err = errno; + close(ctl); + errno = err; + + return -err; +} + +static int is_connected(struct device *idev) +{ + struct fake_input *fake = idev->fake; + struct hidp_conninfo ci; + int ctl; + + /* Fake input */ + if (fake) + return fake->flags & FI_FLAG_CONNECTED; + + /* Standard HID */ + ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); + if (ctl < 0) + return 0; + + memset(&ci, 0, sizeof(ci)); + bacpy(&ci.bdaddr, &idev->dst); + if (ioctl(ctl, HIDPGETCONNINFO, &ci) < 0) { + close(ctl); + return 0; + } + + close(ctl); + + if (ci.state != BT_CONNECTED) + return 0; + else + return 1; +} + +/* + * Input Device methods + */ +static DBusMessage *device_connect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + struct fake_input *fake = idev->fake; + int err; + + if (idev->pending_connect) + return in_progress(msg); + + if (is_connected(idev)) + return already_connected(msg); + + idev->pending_connect = dbus_message_ref(msg); + + /* Fake input device */ + if (fake) { + if (fake->connect(idev) < 0) { + int err = errno; + const char *str = strerror(err); + error("Connect failed: %s(%d)", str, err); + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; + return connection_attempt_failed(msg, err); + } + fake->flags |= FI_FLAG_CONNECTED; + return NULL; + } + + /* HID devices */ + err = bt_l2cap_connect(&idev->src, &idev->dst, L2CAP_PSM_HIDP_CTRL, + 0, control_connect_cb, idev); + if (err < 0) { + error("L2CAP connect failed: %s(%d)", strerror(-err), -err); + dbus_message_unref(idev->pending_connect); + idev->pending_connect = NULL; + return connection_attempt_failed(msg, -err); + } + + return NULL; +} + +static DBusMessage *device_disconnect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + int err; + + err = disconnect(idev, 0); + if (err < 0) + return create_errno_message(msg, -err); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *device_is_connected(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + dbus_bool_t connected = is_connected(idev); + + return g_dbus_create_reply(msg, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); +} + +static DBusMessage *device_get_adapter(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + char addr[18]; + const char *paddr = addr; + + ba2str(&idev->src, addr); + + return g_dbus_create_reply(msg, DBUS_TYPE_STRING, &paddr, + DBUS_TYPE_INVALID); +} + +static DBusMessage *device_get_address(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + char addr[18]; + const char *paddr = addr; + + ba2str(&idev->dst, addr); + + return g_dbus_create_reply(msg, DBUS_TYPE_STRING, &paddr, + DBUS_TYPE_INVALID); +} + +static DBusMessage *device_get_name(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + const char *pname = (idev->name ? idev->name : ""); + + return g_dbus_create_reply(msg, DBUS_TYPE_STRING, &pname, + DBUS_TYPE_INVALID); +} + +static DBusMessage *device_get_product_id(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + + return g_dbus_create_reply(msg, DBUS_TYPE_UINT16, &idev->product, + DBUS_TYPE_INVALID); +} + +static DBusMessage *device_get_vendor_id(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct device *idev = data; + + return g_dbus_create_reply(msg, DBUS_TYPE_UINT16, &idev->vendor, + DBUS_TYPE_INVALID); +} + +static void device_unregister(void *data) +{ + struct device *idev = data; + + /* Disconnect if applied */ + disconnect(idev, (1 << HIDP_VIRTUAL_CABLE_UNPLUG)); + device_free(idev); +} + +static GDBusMethodTable device_methods[] = { + { "Connect", "", "", device_connect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", device_disconnect }, + { "IsConnected", "", "b", device_is_connected }, + { "GetAdapter", "", "s", device_get_adapter }, + { "GetAddress", "", "s", device_get_address }, + { "GetName", "", "s", device_get_name }, + { "GetProductId", "", "q", device_get_product_id }, + { "GetVendorId", "", "q", device_get_vendor_id }, + { } +}; + +static GDBusSignalTable device_signals[] = { + { "Connected", "" }, + { "Disconnected", "" }, + { } +}; + +/* + * Input registration functions + */ +static int register_path(DBusConnection *conn, const char *path, struct device *idev) +{ + if (g_dbus_register_interface(conn, path, INPUT_DEVICE_INTERFACE, + device_methods, device_signals, NULL, + idev, device_unregister) == FALSE) { + error("Failed to register %s interface to %s", + INPUT_DEVICE_INTERFACE, path); + return -1; + } + + devices = g_slist_append(devices, idev); + + info("Created input device: %s", path); + + return 0; +} + +int input_device_register(DBusConnection *conn, bdaddr_t *src, bdaddr_t *dst, + struct hidp_connadd_req *hid, const char **ppath) +{ + struct device *idev; + const char *path; + int err; + + idev = device_new(src, dst, hid->subclass, hid->idle_to); + if (!idev) + return -EINVAL; + + path = create_input_path(idev->major, idev->minor); + if (!path) { + device_free(idev); + return -EINVAL; + } + + idev->path = g_strdup(path); + idev->product = hid->product; + idev->vendor = hid->vendor; + idev->conn = dbus_connection_ref(conn); + + err = register_path(conn, path, idev); + + if (!err && ppath) + *ppath = path; + + return err; +} + +int fake_input_register(DBusConnection *conn, bdaddr_t *src, + bdaddr_t *dst, uint8_t ch, const char **ppath) +{ + struct device *idev; + const char *path; + int err; + + idev = device_new(src, dst, 0, 0); + if (!idev) + return -EINVAL; + + path = create_input_path(idev->major, idev->minor); + if (!path) { + device_free(idev); + return -EINVAL; + } + + idev->path = g_strdup(path); + idev->conn = dbus_connection_ref(conn); + + /* FIXME: Missing set product and vendor */ + + idev->fake = g_new0(struct fake_input, 1); + idev->fake->ch = ch; + idev->fake->connect = rfcomm_connect; + idev->fake->disconnect = fake_disconnect; + + err = register_path(conn, path, idev); + + if (!err && ppath) + *ppath = path; + + return err; +} + +static struct device *find_device(const bdaddr_t *src, const bdaddr_t *dst) +{ + GSList *list; + + for (list = devices; list != NULL; list = list->next) { + struct device *idev = list->data; + + if (!bacmp(&idev->src, src) && !bacmp(&idev->dst, dst)) + return idev; + } + + return NULL; +} + +static struct device *find_device_by_path(const char *path) +{ + GSList *list; + + for (list = devices; list != NULL; list = list->next) { + struct device *idev = list->data; + + if (strcmp(idev->path, path) == 0) + return idev; + } + + return NULL; +} + +int input_device_unregister(DBusConnection *conn, const char *path) +{ + struct device *idev; + + idev = find_device_by_path(path); + if (idev == NULL) + return -EINVAL; + + if (idev->pending_connect) { + /* Pending connection running */ + return -EBUSY; + } + + del_stored_device_info(&idev->src, &idev->dst); + + devices = g_slist_remove(devices, idev); + + /* + * Workaround: if connected, the watch will not be able + * to access the D-Bus data assigned to this path + * because the object path data was destroyed. + */ + if (idev->ctrl_watch) + g_source_remove(idev->ctrl_watch); + + if (idev->intr_watch) { + g_source_remove(idev->intr_watch); + g_dbus_emit_signal(conn, + path, INPUT_DEVICE_INTERFACE, + "Disconnected", DBUS_TYPE_INVALID); + } + + g_dbus_emit_signal(conn, INPUT_PATH, + INPUT_MANAGER_INTERFACE, "DeviceRemoved" , + DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + + g_dbus_unregister_interface(conn, path, INPUT_DEVICE_INTERFACE); + + return 0; +} + +gboolean input_device_is_registered(bdaddr_t *src, bdaddr_t *dst) +{ + struct device *idev = find_device(src, dst); + if (!idev) + return FALSE; + else + return TRUE; +} + +int input_device_set_channel(const bdaddr_t *src, const bdaddr_t *dst, int psm, int nsk) +{ + struct device *idev = find_device(src, dst); + if (!idev) + return -ENOENT; + + switch (psm) { + case L2CAP_PSM_HIDP_CTRL: + idev->ctrl_sk = nsk; + break; + case L2CAP_PSM_HIDP_INTR: + idev->intr_sk = nsk; + break; + } + + return 0; +} + +int input_device_close_channels(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct device *idev = find_device(src, dst); + if (!idev) + return -ENOENT; + + if (idev->ctrl_sk >= 0) { + close(idev->ctrl_sk); + idev->ctrl_sk = -1; + } + + if (idev->intr_sk >= 0) { + close(idev->intr_sk); + idev->intr_sk = -1; + } + + return 0; +} + +static gboolean fake_hid_connect(struct device *dev) +{ + struct fake_hid *fhid = dev->fake->priv; + + return fhid->connect(dev->fake); +} + +static int fake_hid_disconnect(struct device *dev) +{ + struct fake_hid *fhid = dev->fake->priv; + + return fhid->disconnect(dev->fake); +} + +int input_device_connadd(bdaddr_t *src, bdaddr_t *dst) +{ + struct device *idev; + struct fake_hid *fake_hid; + struct fake_input *fake = NULL; + int err; + + idev = find_device(src, dst); + if (!idev) + return -ENOENT; + + fake_hid = get_fake_hid(idev->vendor, idev->product); + if (fake_hid) { + fake = g_try_new0(struct fake_input, 1); + if (!fake) { + err = -ENOMEM; + goto error; + } + + fake->connect = fake_hid_connect; + fake->disconnect = fake_hid_disconnect; + fake->priv = fake_hid; + err = fake_hid_connadd(fake, idev->intr_sk, fake_hid); + } else + err = hidp_connadd(src, dst, idev->ctrl_sk, idev->intr_sk, + idev->timeout, idev->name); + if (err < 0) + goto error; + + idev->intr_watch = create_watch(idev->intr_sk, intr_watch_cb, idev); + idev->ctrl_watch = create_watch(idev->ctrl_sk, ctrl_watch_cb, idev); + g_dbus_emit_signal(idev->conn, + idev->path, + INPUT_DEVICE_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + return 0; + +error: + close(idev->ctrl_sk); + close(idev->intr_sk); + idev->ctrl_sk = -1; + idev->intr_sk = -1; + g_free(fake); + return err; +} diff --git a/input/device.h b/input/device.h new file mode 100644 index 00000000..c9296ad0 --- /dev/null +++ b/input/device.h @@ -0,0 +1,50 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 + * + */ + +#define L2CAP_PSM_HIDP_CTRL 0x11 +#define L2CAP_PSM_HIDP_INTR 0x13 + +struct device; + +struct fake_input { + int flags; + GIOChannel *io; + int uinput; /* uinput socket */ + int rfcomm; /* RFCOMM socket */ + uint8_t ch; /* RFCOMM channel number */ + gboolean (*connect) (struct device *dev); + int (*disconnect) (struct device *dev); + void *priv; +}; + +int input_device_register(DBusConnection *conn, bdaddr_t *src, bdaddr_t *dst, + struct hidp_connadd_req *hidp, const char **ppath); +int fake_input_register(DBusConnection *conn, bdaddr_t *src, + bdaddr_t *dst, uint8_t ch, const char **ppath); +int input_device_unregister(DBusConnection *conn, const char *path); + +gboolean input_device_is_registered(bdaddr_t *src, bdaddr_t *dst); + +int input_device_set_channel(const bdaddr_t *src, const bdaddr_t *dst, int psm, int nsk); +int input_device_close_channels(const bdaddr_t *src, const bdaddr_t *dst); +int input_device_connadd(bdaddr_t *src, bdaddr_t *dst); diff --git a/input/fakehid.c b/input/fakehid.c new file mode 100644 index 00000000..d752d85a --- /dev/null +++ b/input/fakehid.c @@ -0,0 +1,412 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/l2cap.h> +#include <bluetooth/hidp.h> + +#include <glib.h> +#include <dbus/dbus.h> + +#include "logging.h" +#include "device.h" +#include "fakehid.h" +#include "uinput.h" + +#ifndef KEY_REMOTE_1 +#define KEY_REMOTE_1 0x1b6 +#endif +#ifndef KEY_REMOTE_2 +#define KEY_REMOTE_2 0x1b7 +#endif +#ifndef KEY_REMOTE_3 +#define KEY_REMOTE_3 0x1b8 +#endif +#ifndef KEY_REMOTE_4 +#define KEY_REMOTE_4 0x1b9 +#endif +#ifndef KEY_REMOTE_5 +#define KEY_REMOTE_5 0x1ba +#endif +#ifndef KEY_REMOTE_6 +#define KEY_REMOTE_6 0x1bb +#endif +#ifndef KEY_REMOTE_7 +#define KEY_REMOTE_7 0x1bc +#endif +#ifndef KEY_REMOTE_8 +#define KEY_REMOTE_8 0x1bd +#endif +#ifndef KEY_REMOTE_9 +#define KEY_REMOTE_9 0x1be +#endif +#ifndef KEY_REMOTE_0 +#define KEY_REMOTE_0 0x1bf +#endif + +#define PS3_FLAGS_MASK 0xFFFFFF00 + +enum ps3remote_special_keys { + PS3R_BIT_PS = 0, + PS3R_BIT_ENTER = 3, + PS3R_BIT_L2 = 8, + PS3R_BIT_R2 = 9, + PS3R_BIT_L1 = 10, + PS3R_BIT_R1 = 11, + PS3R_BIT_TRIANGLE = 12, + PS3R_BIT_CIRCLE = 13, + PS3R_BIT_CROSS = 14, + PS3R_BIT_SQUARE = 15, + PS3R_BIT_SELECT = 16, + PS3R_BIT_L3 = 17, + PS3R_BIT_R3 = 18, + PS3R_BIT_START = 19, + PS3R_BIT_UP = 20, + PS3R_BIT_RIGHT = 21, + PS3R_BIT_DOWN = 22, + PS3R_BIT_LEFT = 23, +}; + +static unsigned int ps3remote_bits[] = { + [PS3R_BIT_ENTER] = 0x0b, + [PS3R_BIT_PS] = 0x43, + [PS3R_BIT_SQUARE] = 0x5f, + [PS3R_BIT_CROSS] = 0x5e, + [PS3R_BIT_CIRCLE] = 0x5d, + [PS3R_BIT_TRIANGLE] = 0x5c, + [PS3R_BIT_R1] = 0x5b, + [PS3R_BIT_L1] = 0x5a, + [PS3R_BIT_R2] = 0x59, + [PS3R_BIT_L2] = 0x58, + [PS3R_BIT_LEFT] = 0x57, + [PS3R_BIT_DOWN] = 0x56, + [PS3R_BIT_RIGHT] = 0x55, + [PS3R_BIT_UP] = 0x54, + [PS3R_BIT_START] = 0x53, + [PS3R_BIT_R3] = 0x52, + [PS3R_BIT_L3] = 0x51, + [PS3R_BIT_SELECT] = 0x50, +}; + +static unsigned int ps3remote_keymap[] = { + [0x16] = KEY_EJECTCD, + [0x64] = KEY_AUDIO, + [0x65] = KEY_ANGLE, + [0x63] = KEY_SUBTITLE, + [0x0f] = KEY_CLEAR, + [0x28] = KEY_TIME, + [0x00] = KEY_REMOTE_1, + [0x01] = KEY_REMOTE_2, + [0x02] = KEY_REMOTE_3, + [0x03] = KEY_REMOTE_4, + [0x04] = KEY_REMOTE_5, + [0x05] = KEY_REMOTE_6, + [0x06] = KEY_REMOTE_7, + [0x07] = KEY_REMOTE_8, + [0x08] = KEY_REMOTE_9, + [0x09] = KEY_REMOTE_0, + [0x81] = KEY_RED, + [0x82] = KEY_GREEN, + [0x80] = KEY_BLUE, + [0x83] = KEY_YELLOW, + [0x70] = KEY_INFO, /* display */ + [0x1a] = KEY_MENU, /* top menu */ + [0x40] = KEY_CONTEXT_MENU, /* pop up/menu */ + [0x0e] = KEY_ESC, /* return */ + [0x5c] = KEY_OPTION, /* options/triangle */ + [0x5d] = KEY_BACK, /* back/circle */ + [0x5f] = KEY_SCREEN, /* view/square */ + [0x5e] = BTN_0, /* cross */ + [0x54] = KEY_UP, + [0x56] = KEY_DOWN, + [0x57] = KEY_LEFT, + [0x55] = KEY_RIGHT, + [0x0b] = KEY_ENTER, + [0x5a] = BTN_TL, /* L1 */ + [0x58] = BTN_TL2, /* L2 */ + [0x51] = BTN_THUMBL, /* L3 */ + [0x5b] = BTN_TR, /* R1 */ + [0x59] = BTN_TR2, /* R2 */ + [0x52] = BTN_THUMBR, /* R3 */ + [0x43] = KEY_HOMEPAGE, /* PS button */ + [0x50] = KEY_SELECT, + [0x53] = BTN_START, + [0x33] = KEY_REWIND, /* scan back */ + [0x32] = KEY_PLAY, + [0x34] = KEY_FORWARD, /* scan forward */ + [0x30] = KEY_PREVIOUS, + [0x38] = KEY_STOP, + [0x31] = KEY_NEXT, + [0x60] = KEY_FRAMEBACK, /* slow/step back */ + [0x39] = KEY_PAUSE, + [0x61] = KEY_FRAMEFORWARD, /* slow/step forward */ + [0xff] = KEY_MAX, +}; + +static int ps3remote_decode(char *buff, int size, unsigned int *value) +{ + static unsigned int lastkey = 0; + static unsigned int lastmask = 0; + int retval, mask, key, i; + + if (size < 12) { + error("Got a shorter packet! (size %i)\n", size); + return KEY_RESERVED; + } + + mask = (buff[2] << 16) + (buff[3] << 8) + buff[4]; + key = buff[5]; + + /* first, check flags */ + for (i = 0; i < 24; i++) { + if ((lastmask & (1 << i)) == (mask & (1 << i))) + continue; + if (ps3remote_bits[i] == 0) + goto error; + retval = ps3remote_keymap[ps3remote_bits[i]]; + if (mask & (1 << i)) + /* key pressed */ + *value = 1; + else + /* key released */ + *value = 0; + + goto out; + } + + *value = buff[11]; + if (buff[11] == 1) { + retval = ps3remote_keymap[key]; + } else + retval = lastkey; + + if (retval == KEY_RESERVED) + goto error; + if (retval == KEY_MAX) + return retval; + + lastkey = retval; + +out: + fflush(stdout); + + lastmask = mask; + + return retval; + +error: + error("ps3remote: unrecognized sequence [%#x][%#x][%#x][%#x] [%#x]," + "last: [%#x][%#x][%#x][%#x]", + buff[2], buff[3], buff[4], buff[5], buff[11], + lastmask >> 16, lastmask >> 8 & 0xff, + lastmask & 0xff, lastkey); + return -1; +} + +static gboolean ps3remote_event(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct fake_input *fake = data; + struct uinput_event event; + unsigned int key, value = 0; + gsize size; + char buff[50]; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + error("Hangup or error on rfcomm server socket"); + goto failed; + } + + memset(buff, 0, sizeof(buff)); + + if (g_io_channel_read(chan, buff, sizeof(buff), &size) != + G_IO_ERROR_NONE) { + error("IO Channel read error"); + goto failed; + } + + key = ps3remote_decode(buff, size, &value); + if (key == KEY_RESERVED) { + error("Got invalid key from decode"); + goto failed; + } else if (key == KEY_MAX) + return TRUE; + + memset(&event, 0, sizeof(event)); + gettimeofday(&event.time, NULL); + event.type = EV_KEY; + event.code = key; + event.value = value; + if (write(fake->uinput, &event, sizeof(event)) != sizeof(event)) { + error("Error writing to uinput device"); + goto failed; + } + + memset(&event, 0, sizeof(event)); + gettimeofday(&event.time, NULL); + event.type = EV_SYN; + event.code = SYN_REPORT; + if (write(fake->uinput, &event, sizeof(event)) != sizeof(event)) { + error("Error writing to uinput device"); + goto failed; + } + + return TRUE; + +failed: + ioctl(fake->uinput, UI_DEV_DESTROY); + close(fake->uinput); + fake->uinput = -1; + g_io_channel_unref(fake->io); + + return FALSE; +} + +static int ps3remote_setup_uinput(struct fake_input *fake, + struct fake_hid *fake_hid) +{ + struct uinput_dev dev; + int i; + + fake->uinput = open("/dev/input/uinput", O_RDWR); + if (fake->uinput < 0) { + fake->uinput = open("/dev/uinput", O_RDWR); + if (fake->uinput < 0) { + fake->uinput = open("/dev/misc/uinput", O_RDWR); + if (fake->uinput < 0) { + error("Error opening uinput device file"); + return 1; + } + } + } + + memset(&dev, 0, sizeof(dev)); + snprintf(dev.name, sizeof(dev.name), "%s", "PS3 Remote Controller"); + dev.id.bustype = BUS_BLUETOOTH; + dev.id.vendor = fake_hid->vendor; + dev.id.product = fake_hid->product; + + if (write(fake->uinput, &dev, sizeof(dev)) != sizeof(dev)) { + error("Error creating uinput device"); + goto err; + } + + /* enabling key events */ + if (ioctl(fake->uinput, UI_SET_EVBIT, EV_KEY) < 0) { + error("Error enabling uinput device key events"); + goto err; + } + + /* enabling keys */ + for (i = 0; i < 256; i++) + if (ps3remote_keymap[i] != KEY_RESERVED) + if (ioctl(fake->uinput, UI_SET_KEYBIT, + ps3remote_keymap[i]) < 0) { + error("Error enabling uinput key %i", + ps3remote_keymap[i]); + goto err; + } + + /* creating the device */ + if (ioctl(fake->uinput, UI_DEV_CREATE) < 0) { + error("Error creating uinput device"); + goto err; + } + + return 0; + +err: + close(fake->uinput); + return 1; +} + +static gboolean fake_hid_common_connect(struct fake_input *fake) +{ + return TRUE; +} + +static int fake_hid_common_disconnect(struct fake_input *fake) +{ + return 0; +} + +static struct fake_hid fake_hid_table[] = { + /* Sony PS3 remote device */ + { + .vendor = 0x054c, + .product = 0x0306, + .connect = fake_hid_common_connect, + .disconnect = fake_hid_common_disconnect, + .event = ps3remote_event, + .setup_uinput = ps3remote_setup_uinput, + }, + + { }, +}; + +static inline int fake_hid_match_device(uint16_t vendor, uint16_t product, + struct fake_hid *fhid) +{ + return vendor == fhid->vendor && product == fhid->product; +} + +struct fake_hid *get_fake_hid(uint16_t vendor, uint16_t product) +{ + int i; + + for (i = 0; fake_hid_table[i].vendor != 0; i++) + if (fake_hid_match_device(vendor, product, &fake_hid_table[i])) + return &fake_hid_table[i]; + + return NULL; +} + +int fake_hid_connadd(struct fake_input *fake, int intr_sk, + struct fake_hid *fake_hid) +{ + if (fake_hid->setup_uinput(fake, fake_hid)) { + error("Error setting up uinput"); + return ENOMEM; + } + + fake->io = g_io_channel_unix_new(intr_sk); + g_io_channel_set_close_on_unref(fake->io, TRUE); + g_io_add_watch(fake->io, G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) fake_hid->event, fake); + + return 0; +} diff --git a/input/fakehid.h b/input/fakehid.h new file mode 100644 index 00000000..3bc2f3d6 --- /dev/null +++ b/input/fakehid.h @@ -0,0 +1,39 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 + * + */ + +struct fake_hid; +struct fake_input; + +struct fake_hid { + uint16_t vendor; + uint16_t product; + gboolean (*connect) (struct fake_input *fake_input); + int (*disconnect) (struct fake_input *fake_input); + gboolean (*event) (GIOChannel *chan, GIOCondition cond, gpointer data); + int (*setup_uinput) (struct fake_input *fake, struct fake_hid *fake_hid); +}; + +struct fake_hid *get_fake_hid(uint16_t vendor, uint16_t product); + +int fake_hid_connadd(struct fake_input *fake, int intr_sk, + struct fake_hid *fake_hid); diff --git a/input/input-api.txt b/input/input-api.txt new file mode 100644 index 00000000..8eb13126 --- /dev/null +++ b/input/input-api.txt @@ -0,0 +1,103 @@ +Bluetooth input service API description +*************************************** + +Copyright (C) 2006-2007 Marcel Holtmann <marcel@holtmann.org> + + +Input Manager hierarchy +======================= + +Interface org.bluez.input.Manager +Object path /org/bluez/input + +Methods array{string} ListDevices() + + Returns an array of available input devices path. + + string CreateDevice(string address) + + Create an input device object. + + On success it will return the path of the + newly created device object. + + Possible errors: org.bluez.Error.AlreadyExists + org.bluez.Error.NotSupported + org.bluez.Error.ConnectionAttemptFailed + org.bluez.Error.Failed + + string CreateSecureDevice(string address) + + Create an input device object. Pairing will + be initiated if needed(keyboard/combo devices). + + On success it will return the path of the + newly created device object. + + Possible errors: org.bluez.Error.AlreadyExists + org.bluez.Error.NotSupported + org.bluez.Error.ConnectionAttemptFailed + org.bluez.Error.Failed + + void RemoveDevice(string path) + + Remove the input device object for a given path. + + Possible errors:org.bluez.Error.DoesNotExist + org.bluez.Error.Failed + +Signals void DeviceCreated(string path) + + void DeviceRemoved(string path) + + +Input Device hierarchy +====================== + +Interface org.bluez.input.Device +Object path /org/bluez/input/{keyboard*|mouse*|...} + +Methods string GetAdapter() + + Returns the adapter address. + + Example: "00:11:22:33:44:55" + + string GetAddress() + + Returns the device address. + + Example: "00:11:22:33:44:55" + + string GetName() + + Returns the service name. + + uint16 GetProductId() + + Returns the product id. + + uint16 GetVendorId() + + Returns the vendor id. + + boolean IsConnected() + + Returns the connection status. + + void Connect() + + Connect to the input device. + + Possible errors: org.bluez.Error.AlreadyConnected + org.bluez.Error.ConnectionAttemptFailed + + void Disconnect() + + Disconnect from the input device. + + Possible errors: org.bluez.Error.Failed + +Signals void Connected() + + void Disconnected() diff --git a/input/input.conf b/input/input.conf new file mode 100644 index 00000000..abfb64f2 --- /dev/null +++ b/input/input.conf @@ -0,0 +1,9 @@ +# Configuration file for the input service + +# This section contains options which are not specific to any +# particular interface +[General] + +# Set idle timeout (in minutes) before the connection will +# be disconnect (defaults to 0 for no timeout) +#IdleTimeout=30 diff --git a/input/main.c b/input/main.c new file mode 100644 index 00000000..b269c76e --- /dev/null +++ b/input/main.c @@ -0,0 +1,154 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> + +#include <gdbus.h> + +#include "plugin.h" +#include "../hcid/device.h" +#include "logging.h" +#include "dbus-service.h" +#include "manager.h" + +#define INPUT_INTERFACE "org.bluez.Input" + +static DBusMessage *input_connect(DBusConnection *conn, + DBusMessage *msg, void *user_data) +{ + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *input_disconnect(DBusConnection *conn, + DBusMessage *msg, void *user_data) +{ + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *input_is_connected(DBusConnection *conn, + DBusMessage *msg, void *user_data) +{ + dbus_bool_t connected = FALSE; + + return g_dbus_create_reply(msg, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); +} + +static GDBusMethodTable input_methods[] = { + { "Connect", "", "", input_connect }, + { "Disconnect", "", "", input_disconnect }, + { "IsConnected", "", "b", input_is_connected }, + { } +}; + +static GDBusSignalTable input_signals[] = { + { "Connected", "" }, + { "Disconnected", "" }, + { } +}; + +static DBusConnection *conn; + +static int input_probe(struct btd_device *device) +{ + DBG("path %s", device->path); + + if (g_dbus_register_interface(conn, device->path, INPUT_INTERFACE, + input_methods, input_signals, NULL, + device, NULL) == FALSE) + return -1; + + return 0; +} + +static void input_remove(struct btd_device *device) +{ + DBG("path %s", device->path); + + g_dbus_unregister_interface(conn, device->path, INPUT_INTERFACE); +} + +static struct btd_device_driver input_driver = { + .name = "input", + .uuids = BTD_UUIDS("00001124-0000-1000-8000-00805f9b34fb"), + .probe = input_probe, + .remove = input_remove, +}; + +static GKeyFile *load_config_file(const char *file) +{ + GKeyFile *keyfile; + GError *err = NULL; + + keyfile = g_key_file_new(); + + if (!g_key_file_load_from_file(keyfile, file, 0, &err)) { + error("Parsing %s failed: %s", file, err->message); + g_error_free(err); + g_key_file_free(keyfile); + return NULL; + } + + return keyfile; +} + +static int input_init(void) +{ + GKeyFile *config; + + conn = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (conn == NULL) + return -EIO; + + config = load_config_file(CONFIGDIR "/input.conf"); + + if (input_manager_init(conn, config) < 0) { + dbus_connection_unref(conn); + return -EIO; + } + + if (config) + g_key_file_free(config); + + btd_register_device_driver(&input_driver); + + return 0; +} + +static void input_exit(void) +{ + btd_unregister_device_driver(&input_driver); + + input_manager_exit(); + + dbus_connection_unref(conn); +} + +BLUETOOTH_PLUGIN_DEFINE("input", input_init, input_exit) diff --git a/input/manager.c b/input/manager.c new file mode 100644 index 00000000..2dfb9b2d --- /dev/null +++ b/input/manager.c @@ -0,0 +1,801 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/hci.h> +#include <bluetooth/hci_lib.h> +#include <bluetooth/hidp.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "logging.h" +#include "textfile.h" + +#include "device.h" +#include "server.h" +#include "error.h" +#include "manager.h" +#include "storage.h" +#include "glib-helper.h" + +struct pending_req { + char *adapter_path; /* Local adapter D-Bus path */ + bdaddr_t src; /* Local adapter BT address */ + bdaddr_t dst; /* Peer BT address */ + DBusConnection *conn; + DBusMessage *msg; + sdp_list_t *pnp_recs; + sdp_list_t *hid_recs; + GIOChannel *ctrl_channel; +}; + +static int idle_timeout = 0; + +static GSList *device_paths = NULL; /* Input registered paths */ + +static DBusConnection *connection = NULL; + +static struct pending_req *pending_req_new(DBusConnection *conn, + DBusMessage *msg, bdaddr_t *src, bdaddr_t *dst) +{ + char adapter[18], adapter_path[32]; + struct pending_req *pr; + int dev_id; + + pr = g_try_new0(struct pending_req, 1); + if (!pr) + return NULL; + + ba2str(src, adapter); + dev_id = hci_devid(adapter); + snprintf(adapter_path, 32, "/org/bluez/hci%d", dev_id); + + pr->adapter_path = g_strdup(adapter_path); + bacpy(&pr->src, src); + bacpy(&pr->dst, dst); + pr->conn = dbus_connection_ref(conn); + pr->msg = dbus_message_ref(msg); + + return pr; +} + +static void pending_req_free(struct pending_req *pr) +{ + if (!pr) + return; + + if (pr->adapter_path) + g_free(pr->adapter_path); + + if (pr->conn) + dbus_connection_unref(pr->conn); + + if (pr->msg) + dbus_message_unref(pr->msg); + + if (pr->pnp_recs) + sdp_list_free(pr->pnp_recs, (sdp_free_func_t) sdp_record_free); + + if (pr->hid_recs) + sdp_list_free(pr->hid_recs, (sdp_free_func_t) sdp_record_free); + + g_free(pr); +} + +static void epox_endian_quirk(unsigned char *data, int size) +{ + /* USAGE_PAGE (Keyboard) 05 07 + * USAGE_MINIMUM (0) 19 00 + * USAGE_MAXIMUM (65280) 2A 00 FF <= must be FF 00 + * LOGICAL_MINIMUM (0) 15 00 + * LOGICAL_MAXIMUM (65280) 26 00 FF <= must be FF 00 + */ + unsigned char pattern[] = { 0x05, 0x07, 0x19, 0x00, 0x2a, 0x00, 0xff, + 0x15, 0x00, 0x26, 0x00, 0xff }; + int i; + + if (!data) + return; + + for (i = 0; i < size - sizeof(pattern); i++) { + if (!memcmp(data + i, pattern, sizeof(pattern))) { + data[i + 5] = 0xff; + data[i + 6] = 0x00; + data[i + 10] = 0xff; + data[i + 11] = 0x00; + } + } +} + +static void extract_hid_record(sdp_record_t *rec, struct hidp_connadd_req *req) +{ + sdp_data_t *pdlist, *pdlist2; + uint8_t attr_val; + + pdlist = sdp_data_get(rec, 0x0101); + pdlist2 = sdp_data_get(rec, 0x0102); + if (pdlist) { + if (pdlist2) { + if (strncmp(pdlist->val.str, pdlist2->val.str, 5)) { + strncpy(req->name, pdlist2->val.str, 127); + strcat(req->name, " "); + } + strncat(req->name, pdlist->val.str, 127 - strlen(req->name)); + } else + strncpy(req->name, pdlist->val.str, 127); + } else { + pdlist2 = sdp_data_get(rec, 0x0100); + if (pdlist2) + strncpy(req->name, pdlist2->val.str, 127); + } + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_PARSER_VERSION); + req->parser = pdlist ? pdlist->val.uint16 : 0x0100; + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_DEVICE_SUBCLASS); + req->subclass = pdlist ? pdlist->val.uint8 : 0; + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_COUNTRY_CODE); + req->country = pdlist ? pdlist->val.uint8 : 0; + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_VIRTUAL_CABLE); + attr_val = pdlist ? pdlist->val.uint8 : 0; + if (attr_val) + req->flags |= (1 << HIDP_VIRTUAL_CABLE_UNPLUG); + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_BOOT_DEVICE); + attr_val = pdlist ? pdlist->val.uint8 : 0; + if (attr_val) + req->flags |= (1 << HIDP_BOOT_PROTOCOL_MODE); + + pdlist = sdp_data_get(rec, SDP_ATTR_HID_DESCRIPTOR_LIST); + if (pdlist) { + pdlist = pdlist->val.dataseq; + pdlist = pdlist->val.dataseq; + pdlist = pdlist->next; + + req->rd_data = g_try_malloc0(pdlist->unitSize); + if (req->rd_data) { + memcpy(req->rd_data, (unsigned char *) pdlist->val.str, + pdlist->unitSize); + req->rd_size = pdlist->unitSize; + epox_endian_quirk(req->rd_data, req->rd_size); + } + } +} + +static void extract_pnp_record(sdp_record_t *rec, struct hidp_connadd_req *req) +{ + sdp_data_t *pdlist; + + pdlist = sdp_data_get(rec, SDP_ATTR_VENDOR_ID); + req->vendor = pdlist ? pdlist->val.uint16 : 0x0000; + + pdlist = sdp_data_get(rec, SDP_ATTR_PRODUCT_ID); + req->product = pdlist ? pdlist->val.uint16 : 0x0000; + + pdlist = sdp_data_get(rec, SDP_ATTR_VERSION); + req->version = pdlist ? pdlist->val.uint16 : 0x0000; +} + +static void interrupt_connect_cb(GIOChannel *chan, int err, + const bdaddr_t *src, const bdaddr_t *dst, + gpointer user_data) +{ + struct pending_req *pr = user_data; + struct hidp_connadd_req hidp; + const char *path; + + memset(&hidp, 0, sizeof(hidp)); + + if (err < 0) { + error("connect(): %s (%d)", strerror(-err), -err); + goto failed; + } + + g_io_channel_close(chan); + g_io_channel_unref(chan); + + hidp.idle_to = idle_timeout * 60; + + extract_hid_record(pr->hid_recs->data, &hidp); + if (pr->pnp_recs) + extract_pnp_record(pr->pnp_recs->data, &hidp); + + store_device_info(&pr->src, &pr->dst, &hidp); + + if (input_device_register(pr->conn, &pr->src, + &pr->dst, &hidp, &path) < 0) { + error_failed(pr->conn, pr->msg, "path registration failed"); + goto cleanup; + } + + g_dbus_emit_signal(pr->conn, INPUT_PATH, + INPUT_MANAGER_INTERFACE, "DeviceCreated", + DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + + device_paths = g_slist_append(device_paths, g_strdup(path)); + + g_dbus_send_reply(pr->conn, pr->msg, DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + + goto cleanup; + +failed: + error_connection_attempt_failed(pr->conn, pr->msg, err); + +cleanup: + g_io_channel_close(pr->ctrl_channel); + g_io_channel_unref(pr->ctrl_channel); + pending_req_free(pr); + + if (hidp.rd_data) + g_free(hidp.rd_data); +} + +static void control_connect_cb(GIOChannel *chan, int err, const bdaddr_t *src, + const bdaddr_t *dst, gpointer user_data) +{ + struct pending_req *pr = user_data; + + if (err < 0) { + error("connect(): %s (%d)", strerror(-err), -err); + goto failed; + } + + /* Set HID control channel */ + pr->ctrl_channel = chan; + + /* Connect to the HID interrupt channel */ + err = bt_l2cap_connect(&pr->src, &pr->dst, L2CAP_PSM_HIDP_INTR, 0, + interrupt_connect_cb, pr); + if (err < 0) { + error("L2CAP connect failed:%s (%d)", strerror(-err), -err); + goto failed; + } + + return; + +failed: + error_connection_attempt_failed(pr->conn, pr->msg, -err); + pending_req_free(pr); +} + +static void create_bonding_reply(DBusPendingCall *call, void *data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + struct pending_req *pr = data; + DBusError derr; + int err; + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + error("CreateBonding failed: %s(%s)", + derr.name, derr.message); + error_failed(pr->conn, pr->msg, "Authentication failed (CreateBonding)"); + dbus_error_free(&derr); + dbus_message_unref(reply); + pending_req_free(pr); + return; + } + + dbus_message_unref(reply); + + err = bt_l2cap_connect(&pr->src, &pr->dst, L2CAP_PSM_HIDP_CTRL, 0, + control_connect_cb, pr); + if (err < 0) { + error("L2CAP connect failed:%s (%d)", strerror(-err), -err); + error_connection_attempt_failed(pr->conn, pr->msg, -err); + pending_req_free(pr); + } +} + +static int create_bonding(struct pending_req *pr) +{ + DBusPendingCall *pending; + DBusMessage *msg; + char address[18], *addr_ptr = address; + + msg = dbus_message_new_method_call("org.bluez", pr->adapter_path, + "org.bluez.Adapter", "CreateBonding"); + if (!msg) { + error("Unable to allocate new method call"); + return -1; + } + + ba2str(&pr->dst, address); + dbus_message_append_args(msg, DBUS_TYPE_STRING, &addr_ptr, DBUS_TYPE_INVALID); + if (dbus_connection_send_with_reply(pr->conn, msg, &pending, -1) == FALSE) { + error("Can't send D-Bus message."); + dbus_message_unref(msg); + return -1; + } + dbus_pending_call_set_notify(pending, create_bonding_reply, pr, NULL); + dbus_pending_call_unref(pending); + dbus_message_unref(msg); + return 0; +} + +static void hid_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct pending_req *pr = user_data; + + if (err < 0) { + error_not_supported(pr->conn, pr->msg); + error("SDP search error: %s (%d)", strerror(-err), -err); + goto fail; + } + + if (!recs || !recs->data) { + error_not_supported(pr->conn, pr->msg); + error("Invalid HID service record length"); + goto fail; + } + + pr->hid_recs = recs; + + if (strcmp("CreateSecureDevice", dbus_message_get_member(pr->msg)) == 0) { + sdp_data_t *d; + + /* Pairing mandatory for keyboard and combo */ + d = sdp_data_get(pr->hid_recs->data, + SDP_ATTR_HID_DEVICE_SUBCLASS); + if (d && (d->val.uint8 & 0x40) && + !has_bonding(&pr->src, &pr->dst)) { + if (create_bonding(pr) < 0) { + error_failed(pr->conn, pr->msg, + "Unable to initialize bonding process"); + goto fail; + } + /* Wait bonding reply */ + return; + } + + /* Otherwise proceede L2CAP connection */ + } + + /* No encryption or link key already exists -- connect control channel */ + err = bt_l2cap_connect(&pr->src, &pr->dst, L2CAP_PSM_HIDP_CTRL, 0, + control_connect_cb, pr); + if (err < 0) { + error("L2CAP connect failed:%s (%d)", strerror(-err), -err); + error_connection_attempt_failed(pr->conn, pr->msg, -err); + goto fail; + } + + /* Wait L2CAP connect */ + return; + +fail: + pending_req_free(pr); +} + +static void pnp_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct pending_req *pr = user_data; + uuid_t uuid; + + if (err < 0) { + error_not_supported(pr->conn, pr->msg); + error("SDP search error: %s (%d)", strerror(-err), -err); + goto fail; + } + + if (!recs || !recs->data) { + error_not_supported(pr->conn, pr->msg); + error("Invalid PnP service record length"); + goto fail; + } + + pr->pnp_recs = recs; + sdp_uuid16_create(&uuid, HID_SVCLASS_ID); + err = bt_search_service(&pr->src, &pr->dst, &uuid, hid_record_cb, + pr, NULL); + if (err < 0) { + error_not_supported(pr->conn, pr->msg); + error("HID service search request failed"); + goto fail; + } + + return; + +fail: + pending_req_free(pr); +} + +static void headset_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct pending_req *pr = user_data; + sdp_record_t *rec; + sdp_list_t *protos; + const char *path; + uint8_t ch; + + if (err < 0) { + error_not_supported(pr->conn, pr->msg); + error("SDP search error: %s (%d)", strerror(-err), -err); + goto fail; + } + + if (!recs || !recs->data) { + error_not_supported(pr->conn, pr->msg); + error("Invalid headset service record length"); + goto fail; + } + + rec = recs->data; + + if (sdp_get_access_protos(rec, &protos) < 0) { + error_not_supported(pr->conn, pr->msg); + goto fail; + } + + ch = sdp_get_proto_port(protos, RFCOMM_UUID); + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + sdp_record_free(rec); + + if (ch <= 0) { + error_not_supported(pr->conn, pr->msg); + error("Invalid RFCOMM channel"); + goto fail; + } + + /* FIXME: Store the fake input data */ + + if (fake_input_register(pr->conn, &pr->src, &pr->dst, ch, &path) < 0) { + error("D-Bus path registration failed:%s", path); + error_failed(pr->conn, pr->msg, "Path registration failed"); + goto fail; + } + + g_dbus_emit_signal(pr->conn, INPUT_PATH, + INPUT_MANAGER_INTERFACE, "DeviceCreated", + DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + + device_paths = g_slist_append(device_paths, g_strdup(path)); + + g_dbus_send_reply(pr->conn, pr->msg, DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + +fail: + pending_req_free(pr); +} + +static inline DBusMessage *adapter_not_available(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", + "Adapter not available"); +} + +static inline DBusMessage *already_exists(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".AlreadyExists", + "Input Already exists"); +} + +static inline DBusMessage *not_supported(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".NotSupported", + "Not supported"); +} + +static inline DBusMessage *does_not_exist(DBusMessage *msg) +{ + return g_dbus_create_error(msg, ERROR_INTERFACE ".AlreadyExists", + "Input doesn't exist"); +} + +static DBusMessage *create_device(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct pending_req *pr; + const char *addr; + bdaddr_t src, dst; + uint32_t cls = 0; + int dev_id, err; + uuid_t uuid; + bt_callback_t cb; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &addr, + DBUS_TYPE_INVALID)) + return NULL; + + /* Get the default adapter */ + dev_id = hci_get_route(NULL); + if (dev_id < 0) { + error("Bluetooth adapter not available"); + return adapter_not_available(msg); + } + + if (hci_devba(dev_id, &src) < 0) { + error("Can't get local adapter device info"); + return adapter_not_available(msg); + } + + str2ba(addr, &dst); + if (input_device_is_registered(&src, &dst)) + return already_exists(msg); + + if (read_device_class(&src, &dst, &cls) < 0) { + error("Device class not available"); + return not_supported(msg); + } + + pr = pending_req_new(conn, msg, &src, &dst); + if (!pr) + return NULL; + + switch (cls & 0x1f00) { + case 0x0500: /* Peripheral */ + case 0x0200: /* Phone */ + sdp_uuid16_create(&uuid, PNP_INFO_SVCLASS_ID); + cb = pnp_record_cb; + break; + case 0x0400: /* Fake input */ + sdp_uuid16_create(&uuid, HEADSET_SVCLASS_ID); + cb = headset_record_cb; + break; + default: + pending_req_free(pr); + return not_supported(msg); + } + + err = bt_search_service(&src, &dst, &uuid, cb, pr, NULL); + if (err < 0) { + pending_req_free(pr); + return not_supported(msg); + } + + return NULL; +} + +static DBusMessage *remove_device(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + GSList *l; + const char *path; + int err; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID)) + return NULL; + + l = g_slist_find_custom(device_paths, path, (GCompareFunc) strcmp); + if (!l) + return does_not_exist(msg); + + err = input_device_unregister(conn, path); + if (err < 0) + return create_errno_message(msg, -err); + + g_free(l->data); + device_paths = g_slist_remove(device_paths, l->data); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *list_devices(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, iter_array; + DBusMessage *reply; + GSList *paths; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &iter_array); + + for (paths = device_paths; paths != NULL; paths = paths->next) { + const char *ppath = paths->data; + dbus_message_iter_append_basic(&iter_array, + DBUS_TYPE_STRING, &ppath); + } + + dbus_message_iter_close_container(&iter, &iter_array); + + return reply; +} + +static void manager_unregister(void *data) +{ + info("Unregistered manager path"); + + g_slist_foreach(device_paths, (GFunc) free, NULL); + + g_slist_free(device_paths); +} + +/* + * Stored inputs registration functions + */ + +static void stored_input(char *key, char *value, void *data) +{ + const char *path; + struct hidp_connadd_req hidp; + bdaddr_t dst, *src = data; + + str2ba(key, &dst); + + memset(&hidp, 0, sizeof(hidp)); + + if (parse_stored_device_info(value, &hidp) < 0) + return; + + /* + * Repeated entries for the same remote device are + * acceptable since the source is different. + */ + if (input_device_register(connection, src, &dst, &hidp, &path) < 0) + goto cleanup; + + device_paths = g_slist_append(device_paths, g_strdup(path)); +cleanup: + if (hidp.rd_data) + g_free(hidp.rd_data); +} + +/* hidd to input transition function */ +static void stored_hidd(char *key, char *value, void *data) +{ + struct hidp_connadd_req hidp; + char *str, filename[PATH_MAX + 1], addr[18]; + bdaddr_t dst, *src = data; + + ba2str(src, addr); + create_name(filename, PATH_MAX, STORAGEDIR, addr, "input"); + + str = textfile_get(filename, key); + if (str) { + /* Skip: entry found in input file */ + free(str); + return; + } + + memset(&hidp, 0, sizeof(hidp)); + + if (parse_stored_hidd(value, &hidp) < 0) + return; + + str2ba(key, &dst); + store_device_info(src, &dst, &hidp); + if (hidp.rd_data) + g_free(hidp.rd_data); +} + +static void register_stored_inputs(void) +{ + char dirname[PATH_MAX + 1]; + char filename[PATH_MAX + 1]; + struct dirent *de; + DIR *dir; + bdaddr_t src; + + snprintf(dirname, PATH_MAX, "%s", STORAGEDIR); + + dir = opendir(dirname); + if (!dir) + return; + + while ((de = readdir(dir)) != NULL) { + if (!isdigit(de->d_name[0])) + continue; + + str2ba(de->d_name, &src); + + /* move the hidd entries to the input storage */ + create_name(filename, PATH_MAX, STORAGEDIR, + de->d_name, "hidd"); + textfile_foreach(filename, stored_hidd, &src); + + /* load the input stored devices */ + create_name(filename, PATH_MAX, STORAGEDIR, + de->d_name, "input"); + + textfile_foreach(filename, stored_input, &src); + } + + closedir(dir); +} + +static GDBusMethodTable manager_methods[] = { + { "ListDevices", "", "as", list_devices }, + { "CreateDevice", "s", "s", create_device, + G_DBUS_METHOD_FLAG_ASYNC }, + { "CreateSecureDevice", "s", "s", create_device, + G_DBUS_METHOD_FLAG_ASYNC }, + { "RemoveDevice", "s", "", remove_device }, + { } +}; + +static GDBusSignalTable manager_signals[] = { + { "DeviceCreated", "s" }, + { "DeviceRemoved", "s" }, + { } +}; + +int input_manager_init(DBusConnection *conn, GKeyFile *config) +{ + GError *err = NULL; + + if (config) { + idle_timeout = g_key_file_get_integer(config, "General", + "IdleTimeout", &err); + if (err) { + debug("input.conf: %s", err->message); + g_error_free(err); + } + } + + if (g_dbus_register_interface(conn, INPUT_PATH, INPUT_MANAGER_INTERFACE, + manager_methods, manager_signals, NULL, + NULL, manager_unregister) == FALSE) { + error("Failed to register %s interface to %s", + INPUT_MANAGER_INTERFACE, INPUT_PATH); + return -1; + } + + connection = dbus_connection_ref(conn); + + info("Registered input manager path:%s", INPUT_PATH); + + /* Register well known HID devices */ + register_stored_inputs(); + + server_start(); + + return 0; +} + +void input_manager_exit(void) +{ + g_dbus_unregister_interface(connection, INPUT_PATH, + INPUT_MANAGER_INTERFACE); + + server_stop(); + + dbus_connection_unref(connection); + + connection = NULL; +} diff --git a/input/manager.h b/input/manager.h new file mode 100644 index 00000000..8f9d999e --- /dev/null +++ b/input/manager.h @@ -0,0 +1,28 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 + * + */ + +#define INPUT_PATH "/org/bluez/input" +#define INPUT_MANAGER_INTERFACE "org.bluez.input.Manager" + +int input_manager_init(DBusConnection *conn, GKeyFile *config); +void input_manager_exit(void); diff --git a/input/server.c b/input/server.c new file mode 100644 index 00000000..9267e4aa --- /dev/null +++ b/input/server.c @@ -0,0 +1,153 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <unistd.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/l2cap.h> +#include <bluetooth/hidp.h> +#include <bluetooth/hci.h> +#include <bluetooth/hci_lib.h> +#include <bluetooth/sdp.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "logging.h" + +#include "device.h" +#include "server.h" +#include "storage.h" +#include "dbus-service.h" +#include "glib-helper.h" + +static const char *HID_UUID = "00001124-0000-1000-8000-00805f9b34fb"; + +struct authorization_data { + bdaddr_t src; + bdaddr_t dst; +}; + +static void auth_callback(DBusError *derr, void *user_data) +{ + struct authorization_data *auth = user_data; + + if (derr) { + error("Access denied: %s", derr->message); + if (dbus_error_has_name(derr, DBUS_ERROR_NO_REPLY)) + service_cancel_auth(&auth->src, &auth->dst); + + input_device_close_channels(&auth->src, &auth->dst); + } else + input_device_connadd(&auth->src, &auth->dst); + + g_free(auth); +} + +static int authorize_device(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct authorization_data *auth; + + auth = g_new0(struct authorization_data, 1); + bacpy(&auth->src, src); + bacpy(&auth->dst, dst); + + return service_req_auth(src, dst, HID_UUID, + auth_callback, auth); +} + +static void connect_event_cb(GIOChannel *chan, int err, const bdaddr_t *src, + const bdaddr_t *dst, gpointer data) +{ + int sk, psm = GPOINTER_TO_UINT(data); + + if (err < 0) { + error("accept: %s (%d)", strerror(-err), -err); + return; + } + + sk = g_io_channel_unix_get_fd(chan); + + debug("Incoming connection on PSM %d", psm); + + if (input_device_set_channel(src, dst, psm, sk) < 0) { + /* Send unplug virtual cable to unknown devices */ + if (psm == L2CAP_PSM_HIDP_CTRL) { + unsigned char unplug[] = { 0x15 }; + int err; + err = write(sk, unplug, sizeof(unplug)); + } + close(sk); + return; + } + + if ((psm == L2CAP_PSM_HIDP_INTR) && (authorize_device(src, dst) < 0)) + input_device_close_channels(src, dst); + + return; +} + +static GIOChannel *ctrl_io = NULL; +static GIOChannel *intr_io = NULL; + +int server_start(void) +{ + ctrl_io = bt_l2cap_listen(BDADDR_ANY, L2CAP_PSM_HIDP_CTRL, 0, 0, + connect_event_cb, + GUINT_TO_POINTER(L2CAP_PSM_HIDP_CTRL)); + if (!ctrl_io) { + error("Failed to listen on control channel"); + return -1; + } + g_io_channel_set_close_on_unref(ctrl_io, TRUE); + + intr_io = bt_l2cap_listen(BDADDR_ANY, L2CAP_PSM_HIDP_INTR, 0, 0, + connect_event_cb, + GUINT_TO_POINTER(L2CAP_PSM_HIDP_INTR)); + if (!intr_io) { + error("Failed to listen on interrupt channel"); + g_io_channel_unref(ctrl_io); + ctrl_io = NULL; + } + g_io_channel_set_close_on_unref(intr_io, TRUE); + + return 0; +} + +void server_stop(void) +{ + if (intr_io) + g_io_channel_unref(intr_io); + + if (ctrl_io) + g_io_channel_unref(ctrl_io); +} diff --git a/input/server.h b/input/server.h new file mode 100644 index 00000000..75cf704c --- /dev/null +++ b/input/server.h @@ -0,0 +1,25 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 + * + */ + +int server_start(void); +void server_stop(void); diff --git a/input/storage.c b/input/storage.c new file mode 100644 index 00000000..25894a5b --- /dev/null +++ b/input/storage.c @@ -0,0 +1,364 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 <stdio.h> +#include <errno.h> +#include <ctype.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/file.h> +#include <sys/stat.h> +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/hidp.h> +#include <bluetooth/hci.h> +#include <bluetooth/hci_lib.h> + +#include <glib.h> + +#include "logging.h" +#include "textfile.h" + +#include "storage.h" + +static inline int create_filename(char *buf, size_t size, + bdaddr_t *bdaddr, const char *name) +{ + char addr[18]; + + ba2str(bdaddr, addr); + + return create_name(buf, size, STORAGEDIR, addr, name); +} + +int parse_stored_hidd(const char *str, struct hidp_connadd_req *req) +{ + char tmp[3]; + char *desc; + unsigned int vendor, product, version, subclass, country, parser, pos; + int i; + + desc = malloc(4096); + if (!desc) + return -ENOMEM; + + memset(desc, 0, 4096); + + sscanf(str, "%04X:%04X:%04X %02X %02X %04X %4095s %08X %n", + &vendor, &product, &version, &subclass, &country, + &parser, desc, &req->flags, &pos); + + req->vendor = vendor; + req->product = product; + req->version = version; + req->subclass = subclass; + req->country = country; + req->parser = parser; + + req->rd_size = strlen(desc) / 2; + req->rd_data = g_try_malloc0(req->rd_size); + if (!req->rd_data) { + g_free(desc); + return -ENOMEM; + } + + memset(tmp, 0, sizeof(tmp)); + for (i = 0; i < req->rd_size; i++) { + memcpy(tmp, desc + (i * 2), 2); + req->rd_data[i] = (uint8_t) strtol(tmp, NULL, 16); + } + + g_free(desc); + + return 0; +} + +int parse_stored_device_info(const char *str, struct hidp_connadd_req *req) +{ + char tmp[3]; + const char *desc; + unsigned int vendor, product, version, subclass, country, parser, pos; + size_t len; + int i; + + sscanf(str, "%04X:%04X:%04X %02X %02X %04X %08X %n", + &vendor, &product, &version, &subclass, &country, + &parser, &req->flags, &pos); + + desc = &str[pos]; + len = strlen(desc); + if (len <= 0) + return -ENOENT; + + req->vendor = vendor; + req->product = product; + req->version = version; + req->subclass = subclass; + req->country = country; + req->parser = parser; + + req->rd_size = len / 2; + req->rd_data = g_try_malloc0(req->rd_size); + if (!req->rd_data) { + return -ENOMEM; + } + + memset(tmp, 0, sizeof(tmp)); + for (i = 0; i < req->rd_size; i++) { + memcpy(tmp, desc + (i * 2), 2); + req->rd_data[i] = (uint8_t) strtol(tmp, NULL, 16); + } + + return 0; +} + +int get_stored_device_info(bdaddr_t *src, bdaddr_t *dst, + struct hidp_connadd_req *req) +{ + char filename[PATH_MAX + 1], *str; + char peer[18]; + int err; + + create_filename(filename, PATH_MAX, src, "input"); + + ba2str(dst, peer); + str = textfile_get(filename, peer); + if (!str) + return -ENOENT; + + err = parse_stored_device_info(str, req); + + free(str); + + return err; +} + +int del_stored_device_info(bdaddr_t *src, bdaddr_t *dst) +{ + char filename[PATH_MAX + 1]; + char addr[18]; + + ba2str(dst, addr); + + create_filename(filename, PATH_MAX, src, "hidd"); + textfile_del(filename, addr); + + create_filename(filename, PATH_MAX, src, "input"); + return textfile_del(filename, addr); +} + +int store_device_info(bdaddr_t *src, bdaddr_t *dst, struct hidp_connadd_req *req) +{ + char filename[PATH_MAX + 1], *str, *desc; + int i, err, size; + char addr[18]; + + create_filename(filename, PATH_MAX, src, "input"); + + size = 15 + 3 + 3 + 5 + (req->rd_size * 2) + 2 + 9; + str = g_try_malloc0(size); + if (!str) + return -ENOMEM; + + desc = g_try_malloc0((req->rd_size * 2) + 1); + if (!desc) { + g_free(str); + return -ENOMEM; + } + + for (i = 0; i < req->rd_size; i++) + sprintf(desc + (i * 2), "%2.2X", req->rd_data[i]); + + snprintf(str, size - 1, "%04X:%04X:%04X %02X %02X %04X %08X %s", + req->vendor, req->product, req->version, + req->subclass, req->country, req->parser, + req->flags, desc); + + g_free(desc); + + create_file(filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + + ba2str(dst, addr); + + err = textfile_put(filename, addr, str); + + g_free(str); + + return err; +} + +int read_device_name(bdaddr_t *src, bdaddr_t *dst, char **name) +{ + char filename[PATH_MAX + 1], addr[18], *str; + int len; + + create_filename(filename, PATH_MAX, src, "names"); + + ba2str(dst, addr); + str = textfile_get(filename, addr); + if (!str) + return -ENOENT; + + len = strlen(str); + + /* HID max name size is 128 chars */ + if (len < 128) { + *name = str; + return 0; + } + + *name = g_try_malloc0(128); + if (!*name) + return -ENOMEM; + + snprintf(*name, 128, "%s", str); + + free(str); + + return 0; +} + +int read_device_class(bdaddr_t *src, bdaddr_t *dst, uint32_t *cls) +{ + char filename[PATH_MAX + 1], *str; + char addr[18]; + + ba2str(src, addr); + create_name(filename, PATH_MAX, STORAGEDIR, addr, "classes"); + + ba2str(dst, addr); + str = textfile_get(filename, addr); + if (!str) + return -ENOENT; + + if (sscanf(str, "%x", cls) != 1) { + g_free(str); + return -ENOENT; + } + + g_free(str); + + return 0; +} + +int encrypt_link(bdaddr_t *src, bdaddr_t *dst) +{ + char filename[PATH_MAX + 1]; + struct hci_conn_info_req *cr; + int dd, err, dev_id; + char addr[18], *str; + + create_filename(filename, PATH_MAX, src, "linkkeys"); + + ba2str(dst, addr); + + str = textfile_get(filename, addr); + if (!str) { + error("Encryption link key not found"); + return -ENOKEY; + } + + free(str); + + cr = g_try_malloc0(sizeof(*cr) + sizeof(struct hci_conn_info)); + if (!cr) + return -ENOMEM; + + ba2str(src, addr); + + dev_id = hci_devid(addr); + if (dev_id < 0) { + g_free(cr); + return -errno; + } + + dd = hci_open_dev(dev_id); + if (dd < 0) { + g_free(cr); + return -errno; + } + + bacpy(&cr->bdaddr, dst); + cr->type = ACL_LINK; + + if (ioctl(dd, HCIGETCONNINFO, (unsigned long) cr) < 0) + goto fail; + + if (cr->conn_info->link_mode & HCI_LM_ENCRYPT) { + /* Already encrypted */ + goto done; + } + + if (hci_authenticate_link(dd, htobs(cr->conn_info->handle), 1000) < 0) { + error("Link authentication failed: %s (%d)", + strerror(errno), errno); + goto fail; + } + + if (hci_encrypt_link(dd, htobs(cr->conn_info->handle), 1, 1000) < 0) { + error("Link encryption failed: %s (%d)", + strerror(errno), errno); + goto fail; + } + +done: + g_free(cr); + + hci_close_dev(dd); + + return 0; + +fail: + g_free(cr); + + err = errno; + hci_close_dev(dd); + + return -err; +} + +gboolean has_bonding(bdaddr_t *src, bdaddr_t *dst) +{ + char filename[PATH_MAX + 1]; + char addr[18], *str; + + create_filename(filename, PATH_MAX, src, "linkkeys"); + + ba2str(dst, addr); + + str = textfile_get(filename, addr); + if (!str) + return FALSE; + + free(str); + + return TRUE; +} diff --git a/input/storage.h b/input/storage.h new file mode 100644 index 00000000..90ba4a87 --- /dev/null +++ b/input/storage.h @@ -0,0 +1,41 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 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 + * + */ + +int get_stored_device_info(bdaddr_t *src, bdaddr_t *dst, + struct hidp_connadd_req *req); + +int del_stored_device_info(bdaddr_t *src, bdaddr_t *dst); + +int store_device_info(bdaddr_t *src, bdaddr_t *dst, + struct hidp_connadd_req *req); + +int parse_stored_hidd(const char *str, struct hidp_connadd_req *req); +int parse_stored_device_info(const char *str, + struct hidp_connadd_req *req); + +int read_device_name(bdaddr_t *src, bdaddr_t *dst, char **name); +int read_device_class(bdaddr_t *src, bdaddr_t *dst, uint32_t *cls); + +int encrypt_link(bdaddr_t *src, bdaddr_t *dst); + +gboolean has_bonding(bdaddr_t *src, bdaddr_t *dst); diff --git a/input/test-input b/input/test-input new file mode 100755 index 00000000..7f41b6f6 --- /dev/null +++ b/input/test-input @@ -0,0 +1,45 @@ +#!/usr/bin/python + +import sys +import dbus + +bus = dbus.SystemBus() + +manager = dbus.Interface(bus.get_object('org.bluez', '/org/bluez'), + 'org.bluez.Manager') + +conn = manager.ActivateService('input') + +input = dbus.Interface(bus.get_object(conn, '/org/bluez/input'), + 'org.bluez.input.Manager') + +if (len(sys.argv) < 2): + print "Usage: %s <command>" % (sys.argv[0]) + print "" + print " list" + print " create <address>" + print " remove <path>" + sys.exit(1) + +if (sys.argv[1] == "list"): + list = input.ListDevices() + print list + sys.exit(0) + +if (sys.argv[1] == "create"): + if (len(sys.argv) < 3): + print "Need address parameter" + else: + device = input.CreateSecureDevice(sys.argv[2]) + print device + sys.exit(0) + +if (sys.argv[1] == "remove"): + if (len(sys.argv) < 3): + print "Need object path parameter" + else: + input.RemoveDevice(sys.argv[2]) + sys.exit(0) + +print "Unknown command" +sys.exit(1) |