btsocket: add support for connect()

The original code was written to allow binding to the HCI Control
socket of a specific adapter, and the Kernel mgmt_ops control socket,
and receiving OOB data using recvmsg().

It was then extended to deal with the missing support for the L2CAP,
RFCOMM and SCO address types when binding and creating a server-side
socket.

This now adds support for connecting to L2CAP, RFCOMM and SCO sockets
on remote devices.

BUG=none
TEST=s.connect(...) on a socket created by btsocket.socket()

Change-Id: I6a935d2311c6118135e9ea8ec015564091b67023
diff --git a/btsocket/btsocket.py b/btsocket/btsocket.py
index 0b38a77..37889d2 100644
--- a/btsocket/btsocket.py
+++ b/btsocket/btsocket.py
@@ -15,6 +15,25 @@
     to bind to the HCI monitor and control sockets as well as receive messages
     with ancillary data from them.
 
+    The bind() and connect() functions match the behavior of those of the
+    built-in socket methods and accept a tuple defining the address, the
+    contents of which depend on the protocol of the socket.
+
+    For BTPROTO_HCI this is:
+      @param dev: Device index, or HCI_DEV_NONE.
+      @param channel: Channel, e.g. HCI_CHANNEL_RAW.
+
+    For BTPROTO_L2CAP this is:
+      @param address: Address of device in string format.
+      @param psm: PSM of L2CAP service.
+
+    For BTPROTO_RFCOMM this is:
+      @param address: Address of device in string format.
+      @param cid: Channel ID of RFCOMM service.
+
+    For BTPROTO_SCO this is:
+      @param address: Address of device in string format.
+
     """
 
     def __init__(self,
@@ -24,19 +43,19 @@
         super(BluetoothSocket, self).__init__(family, type, proto)
 
     def bind(self, *args):
-        """Bind the socket.
-
-        For BTPROTO_HCI sockets args should be a tuple containing:
-
-        @param dev device index to bind to, or HCI_DEV_NONE.
-        @param channel channel to bind to, e.g. HCI_CHANNEL_RAW.
-
-        """
+        """Bind the socket to a local address."""
         if self.family == constants.AF_BLUETOOTH:
             return _btsocket.bind(self, self.proto, *args)
         else:
             return super(BluetoothSocket, self).bind(*args)
 
+    def connect(self, *args):
+        """Connect the socket to a remote address."""
+        if self.family == constants.AF_BLUETOOTH:
+            return _btsocket.connect(self, self.proto, *args)
+        else:
+            return super(BluetoothSocket, self).connect(*args)
+
     def recvmsg(self, bufsize, ancbufsize=0, flags=0):
         """Receive normal data and ancillary data from the socket.
 
diff --git a/src/btsocket.c b/src/btsocket.c
index a4e4b62..7b3c579 100644
--- a/src/btsocket.c
+++ b/src/btsocket.c
@@ -18,6 +18,13 @@
 static PyObject *_btsocket_error;
 static PyObject *_btsocket_timeout;
 
+union bt_sockaddr {
+  struct sockaddr_l2 l2;
+  struct sockaddr_rc rc;
+  struct sockaddr_hci hci;
+  struct sockaddr_sco sco;
+};
+
 static int _get_fileno(PyObject *socket) {
   PyObject *fileno_object;
   long fileno;
@@ -69,85 +76,92 @@
   }
 }
 
-static PyObject *_btsocket_bind(PyObject *self, PyObject *args) {
+static PyObject *parsesocketargs(PyObject *self, PyObject *args,
+                                 union bt_sockaddr *addr, size_t *addrlen) {
   PyObject *socket_object, *tuple;
-  union {
-    struct sockaddr_l2 l2;
-    struct sockaddr_rc rc;
-    struct sockaddr_hci hci;
-    struct sockaddr_sco sco;
-  } addr;
-  size_t addrlen;
-  int proto, fd, result;
+  int proto;
 
-  if (!PyArg_ParseTuple(args, "OiO:_btsocket.bind",
+  if (!PyArg_ParseTuple(args, "OiO:_btsocket.parsesocketargs",
                         &socket_object, &proto, &tuple))
     return NULL;
 
-  fd = _get_fileno(socket_object);
-  if (fd < 0)
-    return NULL;
-
-  memset(&addr, 0, sizeof addr);
+  memset(addr, 0, sizeof (union bt_sockaddr));
 
   switch (proto) {
     case BTPROTO_L2CAP: {
       char *straddr;
 
-      addr.l2.l2_family = AF_BLUETOOTH;
+      addr->l2.l2_family = AF_BLUETOOTH;
 
-      if (!PyArg_ParseTuple(tuple, "si:_btsocket.bind",
-                            &straddr, &addr.l2.l2_psm))
+      if (!PyArg_ParseTuple(tuple, "si:_btsocket.parsesocketargs",
+                            &straddr, &addr->l2.l2_psm))
         return NULL;
-      if (!_setbdaddr(straddr, &addr.l2.l2_bdaddr))
+      if (!_setbdaddr(straddr, &addr->l2.l2_bdaddr))
         return NULL;
 
-      addrlen = sizeof (struct sockaddr_l2);
+      *addrlen = sizeof (struct sockaddr_l2);
       break;
     }
     case BTPROTO_RFCOMM: {
       char *straddr;
 
-      addr.rc.rc_family = AF_BLUETOOTH;
+      addr->rc.rc_family = AF_BLUETOOTH;
 
-      if (!PyArg_ParseTuple(tuple, "si:_btsocket.bind",
-                            &straddr, &addr.rc.rc_channel))
+      if (!PyArg_ParseTuple(tuple, "si:_btsocket.parsesocketargs",
+                            &straddr, &addr->rc.rc_channel))
         return NULL;
-      if (!_setbdaddr(straddr, &addr.rc.rc_bdaddr))
+      if (!_setbdaddr(straddr, &addr->rc.rc_bdaddr))
         return NULL;
 
-      addrlen = sizeof (struct sockaddr_rc);
+      *addrlen = sizeof (struct sockaddr_rc);
       break;
     }
     case BTPROTO_HCI: {
-      addr.hci.hci_family = AF_BLUETOOTH;
+      addr->hci.hci_family = AF_BLUETOOTH;
 
-      if (!PyArg_ParseTuple(tuple, "HH:_btsocket.bind",
-                            &addr.hci.hci_dev, &addr.hci.hci_channel))
+      if (!PyArg_ParseTuple(tuple, "HH:_btsocket.parsesocketargs",
+                            &addr->hci.hci_dev, &addr->hci.hci_channel))
         return NULL;
 
-      addrlen = sizeof (struct sockaddr_hci);
+      *addrlen = sizeof (struct sockaddr_hci);
       break;
     }
     case BTPROTO_SCO: {
       char *straddr;
 
-      addr.sco.sco_family = AF_BLUETOOTH;
+      addr->sco.sco_family = AF_BLUETOOTH;
 
-      if (!PyArg_ParseTuple(tuple, "s:_btsocket.bind",
+      if (!PyArg_ParseTuple(tuple, "s:_btsocket.parsesocketargs",
                             &straddr))
         return NULL;
-      if (!_setbdaddr(straddr, &addr.sco.sco_bdaddr))
+      if (!_setbdaddr(straddr, &addr->sco.sco_bdaddr))
         return NULL;
 
-      addrlen = sizeof (struct sockaddr_sco);
+      *addrlen = sizeof (struct sockaddr_sco);
       break;
     }
     default:
-      PyErr_SetString(_btsocket_error, "unknown protocol for bind()");
+      PyErr_SetString(_btsocket_error, "unknown protocol");
       return NULL;
   }
 
+  return socket_object;
+}
+
+static PyObject *_btsocket_bind(PyObject *self, PyObject *args) {
+  PyObject *socket_object;
+  union bt_sockaddr addr;
+  size_t addrlen;
+  int fd, result;
+
+  socket_object = parsesocketargs(self, args, &addr, &addrlen);
+  if (!socket_object)
+    return NULL;
+
+  fd = _get_fileno(socket_object);
+  if (fd < 0)
+    return NULL;
+
   Py_BEGIN_ALLOW_THREADS;
   result = bind(fd, (struct sockaddr *)&addr, addrlen);
   Py_END_ALLOW_THREADS;
@@ -159,6 +173,68 @@
   return Py_None;
 }
 
+static PyObject *_btsocket_connect(PyObject *self, PyObject *args) {
+  PyObject *socket_object;
+  union bt_sockaddr addr;
+  size_t addrlen;
+  int fd, timeout = 0, did_timeout = 0, result;
+  double timeout_secs;
+
+  socket_object = parsesocketargs(self, args, &addr, &addrlen);
+  if (!socket_object)
+    return NULL;
+
+  fd = _get_fileno(socket_object);
+  if (fd < 0)
+    return NULL;
+
+  timeout = _get_timeout(socket_object, &timeout_secs);
+
+  Py_BEGIN_ALLOW_THREADS;
+  result = connect(fd, (struct sockaddr *)&addr, addrlen);
+
+  if (timeout) {
+    struct pollfd pollfd;
+    int timeout_ms;
+    int n;
+
+    pollfd.fd = fd;
+    pollfd.events = POLLIN;
+
+    /* timeout limits are set by Python */
+    timeout_ms = (int)(timeout_secs * 1000);
+    n = poll(&pollfd, 1, timeout_ms);
+    if (n == 0) {
+      did_timeout = 1;
+    } else if (n < 0) {
+      result = n;
+    } else {
+      /* result from connect() is EINPROGRESS, get the real error */
+      socklen_t resultlen = sizeof result;
+      (void)getsockopt(fd, SOL_SOCKET, SO_ERROR, &result, &resultlen);
+      if (result == EISCONN)
+        result = 0;
+      else {
+        errno = result;
+        result = -1;
+      }
+    }
+  }
+  Py_END_ALLOW_THREADS;
+
+  if (did_timeout) {
+    PyErr_SetString(_btsocket_timeout, "timed out");
+    return NULL;
+  }
+  if (result < 0) {
+    PyErr_SetFromErrno(_btsocket_error);
+    return NULL;
+  }
+
+  Py_INCREF(Py_None);
+  return Py_None;
+}
+
 static PyObject *_btsocket_recvmsg(PyObject *self, PyObject *args) {
   PyObject *socket_object, *buffers, *iterator, *cmsg_list = NULL, *addrval;
   PyObject *retval = NULL;
@@ -332,6 +408,8 @@
 static PyMethodDef _btsocket_methods[] = {
   { "bind", _btsocket_bind, METH_VARARGS,
     "Bind a Bluetooth socket to a device and channel" },
+  { "connect", _btsocket_connect, METH_VARARGS,
+    "Connect a Bluetooth socket to a remote address" },
   { "recvmsg", _btsocket_recvmsg, METH_VARARGS,
     "Receive normal and ancillary data from a Bluetooth socket" },