diff --git a/pkg/pypi/libzt/__init__.py b/pkg/pypi/libzt/__init__.py index c40b40a..eacf360 100644 --- a/pkg/pypi/libzt/__init__.py +++ b/pkg/pypi/libzt/__init__.py @@ -1,4 +1,5 @@ from .libzt import * from .sockets import * from .node import * +from .select import * from .version import __version__ diff --git a/src/bindings/python/PythonSockets.cxx b/src/bindings/python/PythonSockets.cxx index 06aaa27..a5e358e 100644 --- a/src/bindings/python/PythonSockets.cxx +++ b/src/bindings/python/PythonSockets.cxx @@ -15,56 +15,35 @@ * @file * * ZeroTier Socket API (Python) + * + * This code derives from the Python standard library: + * + * Lib/socket.py + * Modules/socketmodule.c + * Modules/fcntlmodule.c + * Modules/clinic/fcntlmodule.c.h + * Modules/clinic/selectmodule.c.h + * + * Copyright and license text can be found in pypi packaging directory. + * */ #include "ZeroTierSockets.h" -#include "lwip/inet.h" -#include "lwip/sockets.h" - -#include #ifdef ZTS_ENABLE_PYTHON -int zts_py_setblocking(int fd, int block) +#include "Python.h" +#include "PythonSockets.h" +#include "lwip/inet.h" +#include "lwip/sockets.h" +#include "structmember.h" // PyMemberDef + +#include +#include + +PyObject* set_error(void) { - int new_flags, cur_flags, err = 0; - - Py_BEGIN_ALLOW_THREADS cur_flags = zts_bsd_fcntl(fd, F_GETFL, 0); - - if (cur_flags < 0) { - err = ZTS_ERR_SOCKET; - goto done; - } - - if (! block) { - new_flags |= ZTS_O_NONBLOCK; - } - else { - new_flags &= ~ZTS_O_NONBLOCK; - } - - if (new_flags != cur_flags) { - err = zts_bsd_fcntl(fd, F_SETFL, new_flags); - } - -done: - Py_END_ALLOW_THREADS - - return err; -} - -int zts_py_getblocking(int fd) -{ - int flags; - - Py_BEGIN_ALLOW_THREADS flags = zts_bsd_fcntl(fd, F_GETFL, 0); - Py_END_ALLOW_THREADS - - if (flags < 0) - { - return ZTS_ERR_SOCKET; - } - return flags & ZTS_O_NONBLOCK; + return NULL; // PyErr_SetFromErrno(zts_errno); } static int zts_py_tuple_to_sockaddr(int family, PyObject* addr_obj, struct zts_sockaddr* dst_addr, int* addrlen) @@ -115,14 +94,6 @@ PyObject* zts_py_accept(int fd) return t; } -int zts_py_listen(int fd, int backlog) -{ - if (backlog < 0) { - backlog = 128; - } - return zts_bsd_listen(fd, backlog); -} - int zts_py_bind(int fd, int family, int type, PyObject* addr_obj) { struct zts_sockaddr_storage addrbuf; @@ -211,4 +182,469 @@ PyObject* zts_py_addr_get_str(uint64_t net_id, int family) return t; } +/* list of Python objects and their file descriptor */ +typedef struct { + PyObject* obj; /* owned reference */ + int fd; + int sentinel; /* -1 == sentinel */ +} pylist; + +void reap_obj(pylist fd2obj[ZTS_FD_SETSIZE + 1]) +{ + unsigned int i; + for (i = 0; i < (unsigned int)ZTS_FD_SETSIZE + 1 && fd2obj[i].sentinel >= 0; i++) { + Py_CLEAR(fd2obj[i].obj); + } + fd2obj[0].sentinel = -1; +} + +/* returns NULL and sets the Python exception if an error occurred */ +PyObject* set2list(zts_fd_set* set, pylist fd2obj[ZTS_FD_SETSIZE + 1]) +{ + int i, j, count = 0; + PyObject *list, *o; + int fd; + + for (j = 0; fd2obj[j].sentinel >= 0; j++) { + if (ZTS_FD_ISSET(fd2obj[j].fd, set)) { + count++; + } + } + list = PyList_New(count); + if (! list) { + return NULL; + } + + i = 0; + for (j = 0; fd2obj[j].sentinel >= 0; j++) { + fd = fd2obj[j].fd; + if (ZTS_FD_ISSET(fd, set)) { + o = fd2obj[j].obj; + fd2obj[j].obj = NULL; + /* transfer ownership */ + if (PyList_SetItem(list, i, o) < 0) { + goto finally; + } + i++; + } + } + return list; +finally: + Py_DECREF(list); + return NULL; +} + +/* returns -1 and sets the Python exception if an error occurred, otherwise + returns a number >= 0 +*/ +int seq2set(PyObject* seq, zts_fd_set* set, pylist fd2obj[FD_SETSIZE + 1]) +{ + int max = -1; + unsigned int index = 0; + Py_ssize_t i; + PyObject* fast_seq = NULL; + PyObject* o = NULL; + + fd2obj[0].obj = (PyObject*)0; /* set list to zero size */ + ZTS_FD_ZERO(set); + + fast_seq = PySequence_Fast(seq, "arguments 1-3 must be sequences"); + if (! fast_seq) { + return -1; + } + + for (i = 0; i < PySequence_Fast_GET_SIZE(fast_seq); i++) { + int v; + + /* any intervening fileno() calls could decr this refcnt */ + if (! (o = PySequence_Fast_GET_ITEM(fast_seq, i))) { + goto finally; + } + + Py_INCREF(o); + v = PyObject_AsFileDescriptor(o); + if (v == -1) { + goto finally; + } + +#if defined(_MSC_VER) + max = 0; /* not used for Win32 */ +#else /* !_MSC_VER */ + if (! _PyIsSelectable_fd(v)) { + PyErr_SetString(PyExc_ValueError, "filedescriptor out of range in select()"); + goto finally; + } + if (v > max) { + max = v; + } +#endif /* _MSC_VER */ + ZTS_FD_SET(v, set); + + /* add object and its file descriptor to the list */ + if (index >= (unsigned int)FD_SETSIZE) { + PyErr_SetString(PyExc_ValueError, "too many file descriptors in select()"); + goto finally; + } + fd2obj[index].obj = o; + fd2obj[index].fd = v; + fd2obj[index].sentinel = 0; + fd2obj[++index].sentinel = -1; + } + Py_DECREF(fast_seq); + return max + 1; + +finally: + Py_XDECREF(o); + Py_DECREF(fast_seq); + return -1; +} + +PyObject* zts_py_select(PyObject* module, PyObject* rlist, PyObject* wlist, PyObject* xlist, PyObject* timeout_obj) +{ + pylist rfd2obj[FD_SETSIZE + 1]; + pylist wfd2obj[FD_SETSIZE + 1]; + pylist efd2obj[FD_SETSIZE + 1]; + PyObject* ret = NULL; + zts_fd_set ifdset, ofdset, efdset; + struct timeval tv, *tvp; + int imax, omax, emax, max; + int n; + _PyTime_t timeout, deadline = 0; + + if (timeout_obj == Py_None) { + tvp = (struct timeval*)NULL; + } + else { + if (_PyTime_FromSecondsObject(&timeout, timeout_obj, _PyTime_ROUND_TIMEOUT) < 0) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_SetString(PyExc_TypeError, "timeout must be a float or None"); + } + return NULL; + } + if (_PyTime_AsTimeval(timeout, &tv, _PyTime_ROUND_TIMEOUT) == -1) { + return NULL; + } + if (tv.tv_sec < 0) { + PyErr_SetString(PyExc_ValueError, "timeout must be non-negative"); + return NULL; + } + tvp = &tv; + } + /* Convert iterables to zts_fd_sets, and get maximum fd number + * propagates the Python exception set in seq2set() + */ + rfd2obj[0].sentinel = -1; + wfd2obj[0].sentinel = -1; + efd2obj[0].sentinel = -1; + if ((imax = seq2set(rlist, &ifdset, rfd2obj)) < 0) { + goto finally; + } + if ((omax = seq2set(wlist, &ofdset, wfd2obj)) < 0) { + goto finally; + } + if ((emax = seq2set(xlist, &efdset, efd2obj)) < 0) { + goto finally; + } + + max = imax; + if (omax > max) { + max = omax; + } + if (emax > max) { + max = emax; + } + if (tvp) { + deadline = _PyTime_GetMonotonicClock() + timeout; + } + + do { + Py_BEGIN_ALLOW_THREADS errno = 0; + // struct zts_timeval zts_tvp; + // zts_tvp.tv_sec = tvp.tv_sec; + // zts_tvp.tv_sec = tvp.tv_sec; + + n = zts_bsd_select(max, &ifdset, &ofdset, &efdset, (struct zts_timeval*)tvp); + Py_END_ALLOW_THREADS + + if (errno != EINTR) + { + break; + } + + /* select() was interrupted by a signal */ + if (PyErr_CheckSignals()) { + goto finally; + } + + if (tvp) { + timeout = deadline - _PyTime_GetMonotonicClock(); + if (timeout < 0) { + /* bpo-35310: lists were unmodified -- clear them explicitly */ + ZTS_FD_ZERO(&ifdset); + ZTS_FD_ZERO(&ofdset); + ZTS_FD_ZERO(&efdset); + n = 0; + break; + } + _PyTime_AsTimeval_noraise(timeout, &tv, _PyTime_ROUND_CEILING); + /* retry select() with the recomputed timeout */ + } + } while (1); + +#ifdef MS_WINDOWS + if (n == SOCKET_ERROR) { + PyErr_SetExcFromWindowsErr(PyExc_OSError, WSAGetLastError()); + } +#else + if (n < 0) { + PyErr_SetFromErrno(PyExc_OSError); + } +#endif + else { + /* any of these three calls can raise an exception. it's more + convenient to test for this after all three calls... but + is that acceptable? + */ + rlist = set2list(&ifdset, rfd2obj); + wlist = set2list(&ofdset, wfd2obj); + xlist = set2list(&efdset, efd2obj); + if (PyErr_Occurred()) { + ret = NULL; + } + else { + ret = PyTuple_Pack(3, rlist, wlist, xlist); + } + Py_XDECREF(rlist); + Py_XDECREF(wlist); + Py_XDECREF(xlist); + } + +finally: + reap_obj(rfd2obj); + reap_obj(wfd2obj); + reap_obj(efd2obj); + return ret; +} + +int zts_py_setsockopt(int fd, PyObject* args) +{ + int level; + int optname; + int res; + Py_buffer optval; + int flag; + unsigned int optlen; + PyObject* none; + + // setsockopt(level, opt, flag) + if (PyArg_ParseTuple(args, "iii:setsockopt", &level, &optname, &flag)) { + res = zts_bsd_setsockopt(fd, level, optname, (char*)&flag, sizeof flag); + goto done; + } + + PyErr_Clear(); + // setsockopt(level, opt, None, flag) + if (PyArg_ParseTuple(args, "iiO!I:setsockopt", &level, &optname, Py_TYPE(Py_None), &none, &optlen)) { + assert(sizeof(socklen_t) >= sizeof(unsigned int)); + res = zts_bsd_setsockopt(fd, level, optname, NULL, (socklen_t)optlen); + goto done; + } + + PyErr_Clear(); + // setsockopt(level, opt, buffer) + if (! PyArg_ParseTuple(args, "iiy*:setsockopt", &level, &optname, &optval)) + return NULL; + +#ifdef MS_WINDOWS + if (optval.len > INT_MAX) { + PyBuffer_Release(&optval); + PyErr_Format(PyExc_OverflowError, "socket option is larger than %i bytes", INT_MAX); + return NULL; + } + res = zts_bsd_setsockopt(fd, level, optname, optval.buf, (int)optval.len); +#else + res = zts_bsd_setsockopt(fd, level, optname, optval.buf, optval.len); +#endif + PyBuffer_Release(&optval); + +done: + return res; +} + +PyObject* zts_py_getsockopt(int fd, PyObject* args) +{ + int level; + int optname; + int res; + PyObject* buf; + socklen_t buflen = 0; + int flag = 0; + socklen_t flagsize; + + if (! PyArg_ParseTuple(args, "ii|i:getsockopt", &level, &optname, &buflen)) { + return NULL; + } + if (buflen == 0) { + flagsize = sizeof flag; + res = zts_bsd_getsockopt(fd, level, optname, (void*)&flag, &flagsize); + if (res < 0) { + return set_error(); + } + return PyLong_FromLong(flag); + } + if (buflen <= 0 || buflen > 1024) { + PyErr_SetString(PyExc_OSError, "getsockopt buflen out of range"); + return NULL; + } + buf = PyBytes_FromStringAndSize((char*)NULL, buflen); + if (buf == NULL) { + return NULL; + } + res = zts_bsd_getsockopt(fd, level, optname, (void*)PyBytes_AS_STRING(buf), &buflen); + if (res < 0) { + Py_DECREF(buf); + return set_error(); + } + _PyBytes_Resize(&buf, buflen); + return buf; +} + +PyObject* zts_py_fcntl(int fd, int code, PyObject* arg) +{ + unsigned int int_arg = 0; + int ret; + if (PySys_Audit("fcntl.fcntl", "iiO", fd, code, arg ? arg : Py_None) < 0) { + return NULL; + } + if (arg != NULL) { + int parse_result; + PyErr_Clear(); + parse_result = PyArg_Parse( + arg, + "I;fcntl requires a file or file descriptor," + " an integer and optionally a third integer", + &int_arg); + if (! parse_result) { + return NULL; + } + } + do { + Py_BEGIN_ALLOW_THREADS ret = zts_bsd_fcntl(fd, code, (int)int_arg); + Py_END_ALLOW_THREADS + } while (ret == -1 && zts_errno == ZTS_EINTR); + if (ret < 0) { + return set_error(); + } + return PyLong_FromLong((long)ret); +} + +PyObject* zts_py_ioctl(int fd, unsigned int code, PyObject* ob_arg, int mutate_arg) +{ +#define IOCTL_BUFSZ 1024 + int arg = 0; + int ret; + Py_buffer pstr; + char* str; + Py_ssize_t len; + char buf[IOCTL_BUFSZ + 1]; /* argument plus NUL byte */ + + if (PySys_Audit("fcntl.ioctl", "iIO", fd, code, ob_arg ? ob_arg : Py_None) < 0) { + return NULL; + } + + if (ob_arg != NULL) { + if (PyArg_Parse(ob_arg, "w*:ioctl", &pstr)) { + char* arg; + str = (char*)pstr.buf; + len = pstr.len; + + if (mutate_arg) { + if (len <= IOCTL_BUFSZ) { + memcpy(buf, str, len); + buf[len] = '\0'; + arg = buf; + } + else { + arg = str; + } + } + else { + if (len > IOCTL_BUFSZ) { + PyBuffer_Release(&pstr); + PyErr_SetString(PyExc_ValueError, "ioctl string arg too long"); + return NULL; + } + else { + memcpy(buf, str, len); + buf[len] = '\0'; + arg = buf; + } + } + if (buf == arg) { + Py_BEGIN_ALLOW_THREADS /* think array.resize() */ + ret = zts_bsd_ioctl(fd, code, arg); + Py_END_ALLOW_THREADS + } + else { + ret = zts_bsd_ioctl(fd, code, arg); + } + if (mutate_arg && (len <= IOCTL_BUFSZ)) { + memcpy(str, buf, len); + } + PyBuffer_Release(&pstr); /* No further access to str below this point */ + if (ret < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + if (mutate_arg) { + return PyLong_FromLong(ret); + } + else { + return PyBytes_FromStringAndSize(buf, len); + } + } + + PyErr_Clear(); + if (PyArg_Parse(ob_arg, "s*:ioctl", &pstr)) { + str = (char*)pstr.buf; + len = pstr.len; + if (len > IOCTL_BUFSZ) { + PyBuffer_Release(&pstr); + PyErr_SetString(PyExc_ValueError, "ioctl string arg too long"); + return NULL; + } + memcpy(buf, str, len); + buf[len] = '\0'; + Py_BEGIN_ALLOW_THREADS ret = zts_bsd_ioctl(fd, code, buf); + Py_END_ALLOW_THREADS if (ret < 0) + { + PyBuffer_Release(&pstr); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + PyBuffer_Release(&pstr); + return PyBytes_FromStringAndSize(buf, len); + } + + PyErr_Clear(); + if (! PyArg_Parse( + ob_arg, + "i;ioctl requires a file or file descriptor," + " an integer and optionally an integer or buffer argument", + &arg)) { + return NULL; + } + // Fall-through to outside the 'if' statement. + } + // TODO: Double check that &arg is correct + Py_BEGIN_ALLOW_THREADS ret = zts_bsd_ioctl(fd, code, &arg); + Py_END_ALLOW_THREADS if (ret < 0) + { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + return PyLong_FromLong((long)ret); +#undef IOCTL_BUFSZ +} + #endif // ZTS_ENABLE_PYTHON diff --git a/src/bindings/python/PythonSockets.h b/src/bindings/python/PythonSockets.h index 260c7df..fb636a8 100644 --- a/src/bindings/python/PythonSockets.h +++ b/src/bindings/python/PythonSockets.h @@ -17,26 +17,28 @@ #ifdef ZTS_ENABLE_PYTHON #include "Python.h" +typedef int SOCKET_T; + int zts_py_bind(int fd, int family, int type, PyObject* addro); int zts_py_connect(int fd, int family, int type, PyObject* addro); PyObject* zts_py_accept(int fd); -int zts_py_listen(int fd, int backlog); - PyObject* zts_py_recv(int fd, int len, int flags); int zts_py_send(int fd, PyObject* buf, int flags); int zts_py_close(int fd); -int zts_py_setblocking(int fd, int flag); - -int zts_py_getblocking(int fd); - PyObject* zts_py_addr_get_str(uint64_t net_id, int family); +PyObject* zts_py_select(PyObject* module, PyObject* rlist, PyObject* wlist, PyObject* xlist, PyObject* timeout_obj); + +int zts_py_setsockopt(int fd, PyObject* args); + +PyObject* zts_py_getsockopt(int fd, PyObject* args); + #endif // ZTS_ENABLE_PYTHON #endif // ZTS_PYTHON_SOCKETS_H diff --git a/src/bindings/python/node.py b/src/bindings/python/node.py index 2513cd3..1401224 100644 --- a/src/bindings/python/node.py +++ b/src/bindings/python/node.py @@ -76,6 +76,9 @@ class ZeroTierNode: def node_is_online(self): return libzt.zts_node_is_online() + def node_id(self): + return libzt.zts_node_get_id() + def net_transport_is_ready(self, net_id): return libzt.zts_net_transport_is_ready(net_id) diff --git a/src/bindings/python/select.py b/src/bindings/python/select.py new file mode 100644 index 0000000..3cf8cd4 --- /dev/null +++ b/src/bindings/python/select.py @@ -0,0 +1,7 @@ +import libzt + + +class select: + def select(self, r, w, x, timeout=None): + r, w, x = libzt.zts_py_select(self, r, w, x, timeout) + return r, w + x, [] diff --git a/src/bindings/python/sockets.py b/src/bindings/python/sockets.py index 2bd6fc0..fd32a35 100755 --- a/src/bindings/python/sockets.py +++ b/src/bindings/python/sockets.py @@ -104,13 +104,11 @@ class socket: def fromfd(self, fd, sock_family, sock_type, sock_proto=0): """libzt does not support this (yet)""" - raise NotImplementedError( - "fromfd(): Not supported. OS File descriptors aren't used in libzt." - ) + raise NotImplementedError("ZeroTier does not expose OS-level sockets") def fromshare(self, data): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + """ZeroTier sockets cannot be shared""" + raise NotImplementedError("ZeroTier sockets cannot be shared") def getaddrinfo( self, host, port, sock_family=0, sock_type=0, sock_proto=0, flags=0 @@ -246,21 +244,21 @@ class socket: if err < 0: handle_error(err) - def connect(self, remote_address): + def connect(self, address): """connect(address) Connect the socket to a remote address""" - err = libzt.zts_py_connect(self._fd, self._family, self._type, remote_address) + err = libzt.zts_py_connect(self._fd, self._family, self._type, address) if err < 0: handle_error(err) - def connect_ex(self, remote_address): + def connect_ex(self, address): """connect_ex(address) -> errno Connect to remote host but return low-level result code, and errno on failure This uses a non-thread-safe implementation of errno """ - err = libzt.zts_py_connect(self._fd, self._family, self._type, remote_address) + err = libzt.zts_py_connect(self._fd, self._family, self._type, address) if err < 0: return errno() return err @@ -268,7 +266,7 @@ class socket: def detach(self): """libzt does not support this""" raise NotImplementedError( - "detach(): Not supported. OS File descriptors aren't used in libzt." + "detach(): Not supported. ZeroTier sockets are not OS-level sockets" ) def dup(self): @@ -276,18 +274,19 @@ class socket: raise NotImplementedError("libzt does not support this (yet?)") def fileno(self): - """libzt does not support this""" - raise NotImplementedError("libzt does not support this (yet?)") + """Return ZeroTier socket file descriptor. This is not OS-level. Can + only be used with ZeroTier's version of select""" + return self._fd def get_inheritable(self): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + """ZeroTier sockets cannot be shared. This always returns False""" + return False def getblocking(self): """getblocking() Return True if the socket is in blocking mode, False if it is non-blocking""" - return libzt.zts_py_getblocking(self._fd) + return libzt.zts_get_blocking(self._fd) def getpeername(self): """libzt does not support this (yet)""" @@ -297,17 +296,17 @@ class socket: """libzt does not support this (yet)""" raise NotImplementedError("libzt does not support this (yet?)") - def getsockopt(self, level, optname, buflen): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + def getsockopt(self, level, optname, buflen=None): + """Get a socket option value""" + return libzt.zts_py_getsockopt(self._fd, (level, optname)) def gettimeout(self): """libzt does not support this (yet)""" raise NotImplementedError("libzt does not support this (yet?)") - def ioctl(self, control, option): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + def ioctl(self, request, arg=0, mutate_flag=True): + """Perform I/O control operations""" + return libzt.zts_py_ioctl(self._fd, request, arg, mutate_flag) def listen(self, backlog=None): """listen([backlog]) @@ -322,7 +321,7 @@ class socket: if backlog is None: backlog = -1 # Lower-level code picks default - err = libzt.zts_py_listen(self._fd, backlog) + err = libzt.zts_bsd_listen(self._fd, backlog) if err < 0: handle_error(err) @@ -412,25 +411,25 @@ class socket: raise NotImplementedError("libzt does not support this (yet?)") def set_inheritable(self, inheritable): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + """ZeroTier sockets cannot be shared""" + raise NotImplementedError("ZeroTier sockets cannot be shared") def setblocking(self, flag): """setblocking(flag) Sets the socket to blocking mode if flag=True, non-blocking if flag=False.""" - libzt.zts_py_setblocking(self._fd, flag) + libzt.zts_set_blocking(self._fd, flag) def settimeout(self, value): """libzt does not support this (yet)""" raise NotImplementedError("libzt does not support this (yet?)") - # TODO: value: buffer - # TODO: value: int - # TODO: value: None -> optlen required def setsockopt(self, level, optname, value): - """libzt does not support this (yet)""" - raise NotImplementedError("libzt does not support this (yet?)") + """Set a socket option value""" + err = libzt.zts_py_setsockopt(self._fd, (level, optname, value)) + if err < 0: + handle_error(err) + return err def shutdown(self, how): """shutdown(how)