diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml index b75ad82..e583ac5 100644 --- a/.github/workflows/auto-format.yml +++ b/.github/workflows/auto-format.yml @@ -9,8 +9,11 @@ jobs: - name: Checkout repo uses: actions/checkout@v2 + - name: Install black + run: pip3 install black + - name: Format code - run: ./build.sh format-code + run: ./build.sh format-code "all" - name: Commit changes uses: joseph-henry/add-and-commit@v7 diff --git a/build.sh b/build.sh index ade9729..e8adea0 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,13 @@ # | SYSTEM DISCOVERY AND CONFIGURATION | # ----------------------------------------------------------------------------- +CLANG_FORMAT=clang-format-11 + +PYTHON=python3 +PIP=pip3 + +libzt=$(pwd) + # Find and set cmake CMAKE=cmake3 if [[ $(which $CMAKE) = "" ]]; @@ -356,7 +363,7 @@ host-uninstall() # └── pkg # └── libzt-1.3.4b1-cp39-cp39-macosx_11_0_x86_64.whl # -host-python-wheel() +host-python() { ARTIFACT="python" # Default to release @@ -365,30 +372,47 @@ host-python-wheel() TARGET_BUILD_DIR=$DEFAULT_HOST_BIN_OUTPUT_DIR-$ARTIFACT-$BUILD_TYPE PKG_OUTPUT_DIR=$TARGET_BUILD_DIR/pkg mkdir -p $PKG_OUTPUT_DIR + # Generate new wrapper + #swig -c++ -python -o src/bindings/python/zt_wrap.cxx -Iinclude src/bindings/python/zt.i # Requires setuptools, etc cd pkg/pypi && ./build.sh wheel && cp -f dist/*.whl $PKG_OUTPUT_DIR + echo -e "\nFinished wheel:\n" + echo $PKG_OUTPUT_DIR/*.whl + + # Test Python wheel + if [[ $2 = *"test"* ]]; then + if [[ -z "${alice_path}" ]]; then + echo "Please set necessary environment variables for test" + exit 0 + fi + pip3 uninstall -y libzt + pip3 install $PKG_OUTPUT_DIR/*.whl + cd $libzt + $PYTHON test/selftest.py server $alice_path $testnet $port4 & + $PYTHON test/selftest.py client $bob_path $testnet $alice_ip4 $port4 & + fi } # Build shared library with python wrapper symbols exported -host-python() -{ - ARTIFACT="python" - # Default to release - BUILD_TYPE=${1:-release} - VARIANT="-DZTS_ENABLE_PYTHON=True" - CACHE_DIR=$DEFAULT_HOST_BUILD_CACHE_DIR-$ARTIFACT-$BUILD_TYPE - TARGET_BUILD_DIR=$DEFAULT_HOST_BIN_OUTPUT_DIR-$ARTIFACT-$BUILD_TYPE - LIB_OUTPUT_DIR=$TARGET_BUILD_DIR/lib - rm -rf $LIB_OUTPUT_DIR - mkdir -p $LIB_OUTPUT_DIR - # Optional step to generate new SWIG wrapper - swig -c++ -python -o src/bindings/python/zt_wrap.cxx -Iinclude src/bindings/python/zt.i - $CMAKE $VARIANT -H. -B$CACHE_DIR -DCMAKE_BUILD_TYPE=$BUILD_TYPE - $CMAKE --build $CACHE_DIR $BUILD_CONCURRENCY - cp -f $CACHE_DIR/lib/$SHARED_LIB_NAME $LIB_OUTPUT_DIR/_libzt.so - echo -e "\n - Build cache : $CACHE_DIR\n - Build output : $BUILD_OUTPUT_DIR\n" - $TREE $TARGET_BUILD_DIR -} +#host-python() +#{ +# ARTIFACT="python" +# # Default to release +# BUILD_TYPE=${1:-release} +# VARIANT="-DZTS_ENABLE_PYTHON=True" +# CACHE_DIR=$DEFAULT_HOST_BUILD_CACHE_DIR-$ARTIFACT-$BUILD_TYPE +# TARGET_BUILD_DIR=$DEFAULT_HOST_BIN_OUTPUT_DIR-$ARTIFACT-$BUILD_TYPE +# LIB_OUTPUT_DIR=$TARGET_BUILD_DIR/lib +# rm -rf $LIB_OUTPUT_DIR +# mkdir -p $LIB_OUTPUT_DIR +# # Optional step to generate new SWIG wrapper +# swig -c++ -python -o src/bindings/python/zt_wrap.cxx -Iinclude src/bindings/python/zt.i +# $CMAKE $VARIANT -H. -B$CACHE_DIR -DCMAKE_BUILD_TYPE=$BUILD_TYPE +# $CMAKE --build $CACHE_DIR $BUILD_CONCURRENCY +# cp -f $CACHE_DIR/lib/$SHARED_LIB_NAME $LIB_OUTPUT_DIR/_libzt.so +# echo -e "\n - Build cache : $CACHE_DIR\n - Build output : $BUILD_OUTPUT_DIR\n" +# $TREE $TARGET_BUILD_DIR +#} # Build shared library with P/INVOKE wrapper symbols exported host-pinvoke() @@ -579,26 +603,46 @@ test() format-code() { - if [[ ! $(which clang-format) = "" ]]; - then - # Eventually: find . -path ./ext -prune -false -o -type f \( -iname \*.c -o -iname \*.h -o -iname \*.cpp -o -iname \*.hpp \) -exec clang-format -i {} \; - clang-format-11 -i include/*.h \ - src/*.c \ - src/*.cpp \ - src/*.hpp \ - examples/c/*.c \ - examples/csharp/*.cs \ - examples/java/*.java \ - test/*.c \ - test/*.cs \ - src/bindings/csharp/*.cs \ - src/bindings/csharp/*.cxx \ - src/bindings/java/*.java \ - src/bindings/java/*.cxx \ - examples/csharp/*.cs - return 0 - else - echo "Please install clang-format." + if [[ $1 = *"all"* ]]; then + format-code "clang" + format-code "python" + fi + + # Clang-format + if [[ $1 = *"clang"* ]]; then + if [[ ! $(which $CLANG_FORMAT) = "" ]]; + then + # Eventually: find . -path ./ext -prune -false -o -type f \( -iname \*.c -o -iname \*.h -o -iname \*.cpp -o -iname \*.hpp \) -exec clang-format -i {} \; + $CLANG_FORMAT -i include/*.h \ + src/*.c \ + src/*.cpp \ + src/*.hpp \ + examples/c/*.c \ + examples/csharp/*.cs \ + examples/java/*.java \ + test/*.c \ + test/*.cs \ + src/bindings/csharp/*.cs \ + src/bindings/csharp/*.cxx \ + src/bindings/java/*.java \ + src/bindings/java/*.cxx \ + examples/csharp/*.cs \ + src/bindings/python/*.cxx \ + src/bindings/python/*.h + return 0 + else + echo "Please install clang-format" + fi + fi + # Python + if [[ $1 = *"python"* ]]; then + if [[ ! $($PIP list | grep black) = "" ]]; + then + $PYTHON -m black src/bindings/python/*.py + $PYTHON -m black examples/python/*.py + else + echo "Please install python module (black)" + fi fi } @@ -637,6 +681,8 @@ clean() rm -rf $ANDROID_PKG_PROJ_DIR/app/build rm -rf $ANDROID_PKG_PROJ_DIR/app/src/main/java/com/zerotier/libzt/*.java rm -rf $ANDROID_PKG_PROJ_DIR/app/.externalNativeBuild + # Python pkg + cd pkg/pypi && ./build.sh clean # Remove whatever remains find . \ \( -name '*.dylib' \ diff --git a/examples/python/example.py b/examples/python/example.py index 58c3b97..41e18e9 100644 --- a/examples/python/example.py +++ b/examples/python/example.py @@ -1,106 +1,92 @@ -'''Example low-level socket usage''' +"""Example low-level socket usage""" import time import sys import libzt + def print_usage(): - '''print help''' + """print help""" print( - "\nUsage: \n" + "\nUsage: \n" ) - print("Ex: python3 demo.py server . 0123456789abcdef 9994 8080") - print("Ex: python3 demo.py client . 0123456789abcdef 9994 192.168.22.1 8080\n") - if len(sys.argv) < 6: + print("Ex: python3 example.py server . 0123456789abcdef 8080") + print("Ex: python3 example.py client . 0123456789abcdef 192.168.22.1 8080\n") + if len(sys.argv) < 5: print("Too few arguments") - if len(sys.argv) > 7: + if len(sys.argv) > 6: print("Too many arguments") sys.exit(0) -is_joined = False # Flags to keep state -is_online = False # Flags to keep state +# +# (Optional) Event handler +# +def on_zerotier_event(event_code, id): + if event_code == libzt.ZTS_EVENT_NODE_ONLINE: + print("ZTS_EVENT_NODE_ONLINE (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NODE_OFFLINE: + print("ZTS_EVENT_NODE_OFFLINE (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NETWORK_READY_IP4: + print("ZTS_EVENT_NETWORK_READY_IP4 (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NETWORK_READY_IP6: + print("ZTS_EVENT_NETWORK_READY_IP6 (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_PEER_DIRECT: + print("ZTS_EVENT_PEER_DIRECT (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_PEER_RELAY: + print("ZTS_EVENT_PEER_RELAY (" + str(event_code) + ") : " + hex(id)) -# -# Event handler -# -class MyEventCallbackClass(libzt.EventCallbackClass): - def on_zerotier_event(self, msg): - global is_online - global is_joined - print("eventCode=", msg.eventCode) - if msg.eventCode == libzt.ZTS_EVENT_NODE_ONLINE: - print("ZTS_EVENT_NODE_ONLINE") - print("nodeId=" + hex(msg.node.address)) - # The node is now online, you can join/leave networks - is_online = True - if msg.eventCode == libzt.ZTS_EVENT_NODE_OFFLINE: - print("ZTS_EVENT_NODE_OFFLINE") - if msg.eventCode == libzt.ZTS_EVENT_NETWORK_READY_IP4: - print("ZTS_EVENT_NETWORK_READY_IP4") - is_joined = True - # The node has successfully joined a network and has an address - # you can perform network calls now - if msg.eventCode == libzt.ZTS_EVENT_PEER_DIRECT: - print("ZTS_EVENT_PEER_DIRECT") - if msg.eventCode == libzt.ZTS_EVENT_PEER_RELAY: - print("ZTS_EVENT_PEER_RELAY") # # Main # def main(): - global is_online - global is_joined + mode = None # client|server + storage_path = "." # Where identity files are stored + net_id = 0 # Network to join + remote_ip = None # ZeroTier IP of remote node + remote_port = 8080 # ZeroTier port your app logic may use - key_file_path = "." # Where identity files are stored - network_id = 0 # Network to join - # Port used by ZeroTier to send encpryted UDP traffic - # NOTE: Should be different from other instances of ZeroTier - # running on the same machine - zt_service_port = 9997 - remote_ip = None # ZeroTier IP of remote node - remote_port = 8080 # ZeroTier port your app logic may use - mode = None # client|server - - if len(sys.argv) < 6 or len(sys.argv) > 7: + if len(sys.argv) < 5 or len(sys.argv) > 6: print_usage() - if sys.argv[1] == "server" and len(sys.argv) == 6: + if sys.argv[1] == "server" and len(sys.argv) == 5: mode = sys.argv[1] - key_file_path = sys.argv[2] - network_id = int(sys.argv[3], 16) - zt_service_port = int(sys.argv[4]) + storage_path = sys.argv[2] + net_id = int(sys.argv[3], 16) + remote_port = int(sys.argv[4]) + if sys.argv[1] == "client" and len(sys.argv) == 6: + mode = sys.argv[1] + storage_path = sys.argv[2] + net_id = int(sys.argv[3], 16) + remote_ip = sys.argv[4] remote_port = int(sys.argv[5]) - if sys.argv[1] == "client" and len(sys.argv) == 7: - mode = sys.argv[1] - key_file_path = sys.argv[2] - network_id = int(sys.argv[3], 16) - zt_service_port = int(sys.argv[4]) - remote_ip = sys.argv[5] - remote_port = int(sys.argv[6]) if mode is None: print_usage() - print("mode = ", mode) - print("path = ", key_file_path) - print("network_id = ", network_id) - print("zt_service_port = ", zt_service_port) - print("remote_ip = ", remote_ip) - print("remote_port = ", remote_port) + print("mode = ", mode) + print("storage_path = ", storage_path) + print("net_id = ", net_id) + print("remote_ip = ", remote_ip) + print("remote_port = ", remote_port) # - # Example start and join logic + # Node initialization and start # print("Starting ZeroTier...") - event_callback = MyEventCallbackClass() - libzt.start(key_file_path, event_callback, zt_service_port) + + n = libzt.ZeroTierNode() + n.init_set_event_handler(on_zerotier_event) # Optional + n.init_from_storage(storage_path) # Optional + n.init_set_port(9994) # Optional + n.node_start() + print("Waiting for node to come online...") - while not is_online: + while not n.node_is_online(): + time.sleep(1) + print("Joining network:", hex(net_id)) + n.net_join(net_id) + while not n.net_transport_is_ready(net_id): time.sleep(1) - print("Joining network:", hex(network_id)) - libzt.join(network_id) - while not is_joined: - time.sleep(1) # You can ping this app at this point print("Joined network") # @@ -140,7 +126,7 @@ def main(): try: print("connecting...") client.connect((remote_ip, remote_port)) - data = "Hello, world!" + data = "Hello, roots!" print("send: ", data) sent_bytes = client.send(data) print("sent: " + str(sent_bytes) + " byte(s)") @@ -150,5 +136,6 @@ def main(): print(ex) print("errno=", libzt.errno()) + if __name__ == "__main__": main() diff --git a/include/ZeroTierSockets.h b/include/ZeroTierSockets.h index d254818..c5ab7cf 100644 --- a/include/ZeroTierSockets.h +++ b/include/ZeroTierSockets.h @@ -1058,6 +1058,10 @@ int zts_py_getblocking(int fd); // Central API // //----------------------------------------------------------------------------// +#ifdef ZTS_ENABLE_PYTHON +#define ZTS_DISABLE_CENTRAL_API 1 +#endif + #ifndef ZTS_DISABLE_CENTRAL_API #define ZTS_CENTRAL_DEFAULT_URL "https://my.zerotier.com" diff --git a/pkg/pypi/MANIFEST.in b/pkg/pypi/MANIFEST.in deleted file mode 100644 index bfa060a..0000000 --- a/pkg/pypi/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -README.md -setup.cfg -setup.py diff --git a/pkg/pypi/build.sh b/pkg/pypi/build.sh index f8554b5..e9dea41 100755 --- a/pkg/pypi/build.sh +++ b/pkg/pypi/build.sh @@ -8,7 +8,7 @@ ext() # Symbolic link to source tree so that sdist structure makes sense ln -s ../../ native # Re-build wrapper to export C symbols - swig -c++ -python -o native/src/bindings/python/zt_wrap.cpp -I./native/include native/src/bindings/python/zt.i + swig -c++ -python -o native/src/bindings/python/zt_wrap.cxx -I./native/include native/src/bindings/python/zt.i # Copy language bindings into module directory cp -f native/src/bindings/python/*.py libzt/ cp -f native/LICENSE.txt LICENSE @@ -30,6 +30,7 @@ clean() find . -name '__pycache__' -type d -delete rm -rf libzt/sockets.py rm -rf libzt/libzt.py + rm -rf libzt/node.py rm -rf src ext build dist native rm -rf libzt.egg-info rm -rf LICENSE diff --git a/pkg/pypi/libzt/__init__.py b/pkg/pypi/libzt/__init__.py index 8e5a8ac..c40b40a 100644 --- a/pkg/pypi/libzt/__init__.py +++ b/pkg/pypi/libzt/__init__.py @@ -1,3 +1,4 @@ from .libzt import * from .sockets import * +from .node import * from .version import __version__ diff --git a/pkg/pypi/libzt/version.py b/pkg/pypi/libzt/version.py index 1956efa..0cacafc 100644 --- a/pkg/pypi/libzt/version.py +++ b/pkg/pypi/libzt/version.py @@ -1 +1 @@ -__version__ = "1.3.4b1" +__version__ = "1.4.0a0" diff --git a/pkg/pypi/setup.cfg b/pkg/pypi/setup.cfg index 239590c..af8363d 100644 --- a/pkg/pypi/setup.cfg +++ b/pkg/pypi/setup.cfg @@ -1,6 +1,6 @@ [metadata] version = attr: libzt.__version__ -description-file = README.md +description_file = README.md license_files = native/LICENSE.txt native/ext/THIRDPARTY.txt diff --git a/pkg/pypi/setup.py b/pkg/pypi/setup.py index 9597092..c304f6e 100644 --- a/pkg/pypi/setup.py +++ b/pkg/pypi/setup.py @@ -19,6 +19,27 @@ class BinaryDistribution(Distribution): def is_pure(self): return False +# monkey-patch for parallel compilation +# Copied from: https://stackoverflow.com/a/13176803 +def parallelCCompile(self, sources, output_dir=None, macros=None, include_dirs=None, debug=0, extra_preargs=None, extra_postargs=None, depends=None): + # those lines are copied from distutils.ccompiler.CCompiler directly + macros, objects, extra_postargs, pp_opts, build = self._setup_compile(output_dir, macros, include_dirs, sources, depends, extra_postargs) + cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) + # parallel code + N=16 # number of parallel compilations + import multiprocessing.pool + def _single_compile(obj): + try: src, ext = build[obj] + except KeyError: return + self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) + # convert to list, imap is evaluated on-demand + list(multiprocessing.pool.ThreadPool(N).imap(_single_compile,objects)) + return objects +import distutils.ccompiler +distutils.ccompiler.CCompiler.compile=parallelCCompile + +# Build + cpp_glob = [] c_glob = [] @@ -30,7 +51,7 @@ if os.name == 'nt': # Everything else else: - cpp_glob.extend(list(glob.glob('native/src/bindings/python/*.cpp'))) + cpp_glob.extend(list(glob.glob('native/src/bindings/python/*.cxx'))) cpp_glob.extend(list(glob.glob('native/src/*.cpp'))) cpp_glob.extend(list(glob.glob('native/ext/ZeroTierOne/node/*.cpp'))) cpp_glob.extend(list(glob.glob('native/ext/ZeroTierOne/osdep/OSUtils.cpp'))) @@ -44,13 +65,14 @@ else: 'native/ext/lwip/src/include', 'native/ext/lwip-contrib/ports/unix/port/include', 'native/ext/ZeroTierOne/include', + 'native/ext/ZeroTierOne', 'native/ext/ZeroTierOne/node', 'native/ext/ZeroTierOne/service', 'native/ext/ZeroTierOne/osdep', 'native/ext/ZeroTierOne/controller'] libzt_module = Extension('libzt._libzt', - extra_compile_args=['-std=c++11', '-DZTS_ENABLE_PYTHON=1', '-DZT_SDK'], + extra_compile_args=['-std=c++11', '-DZTS_ENABLE_PYTHON=1', '-DZT_SDK', '-Wno-parentheses-equality', '-Wno-macro-redefined', '-Wno-tautological-overlap-compare', '-Wno-tautological-constant-out-of-range-compare'], sources=cpp_glob, include_dirs=my_include_dirs) # Separate C library, this is needed since C++ compiler flags are applied diff --git a/src/Central.cpp b/src/Central.cpp index bb99d56..247d0b2 100644 --- a/src/Central.cpp +++ b/src/Central.cpp @@ -11,12 +11,13 @@ */ /****/ +#include "ZeroTierSockets.h" + #ifndef ZTS_DISABLE_CENTRAL_API #include "Debug.hpp" #include "Mutex.hpp" #include "OSUtils.hpp" -#include "ZeroTierSockets.h" #include #include diff --git a/src/Controls.cpp b/src/Controls.cpp index bb2aea0..f0329d1 100644 --- a/src/Controls.cpp +++ b/src/Controls.cpp @@ -19,6 +19,7 @@ #include "Events.hpp" #include "NodeService.hpp" +#include "Signals.hpp" #include "VirtualTap.hpp" #include @@ -65,7 +66,7 @@ int init_subsystems() return ZTS_ERR_SERVICE; } #ifdef ZTS_ENABLE_CUSTOM_SIGNAL_HANDLERS - _install_signal_handlers(); + zts_install_signal_handlers(); #endif // ZTS_ENABLE_CUSTOM_SIGNAL_HANDLERS if (! zts_service) { zts_service = new NodeService(); diff --git a/src/Signals.cpp b/src/Signals.cpp index 7769f6c..a79ea1f 100644 --- a/src/Signals.cpp +++ b/src/Signals.cpp @@ -36,7 +36,7 @@ #include #include -void _signal_handler(int signal) +void zts_signal_handler(int signal) { /* switch(signal) @@ -65,15 +65,15 @@ void _signal_handler(int signal) exit(signal); } -void _install_signal_handlers() +void zts_install_signal_handlers() { - signal(SIGINT, &_signal_handler); + signal(SIGINT, &zts_signal_handler); /* - signal(SIGABRT, &_signal_handler); - signal(SIGFPE, &_signal_handler); - signal(SIGILL, &_signal_handler); - signal(SIGSEGV, &_signal_handler); - signal(SIGTERM, &_signal_handler); + signal(SIGABRT, &zts_signal_handler); + signal(SIGFPE, &zts_signal_handler); + signal(SIGILL, &zts_signal_handler); + signal(SIGSEGV, &zts_signal_handler); + signal(SIGTERM, &zts_signal_handler); */ } diff --git a/src/Signals.hpp b/src/Signals.hpp index e621e1a..d7c25cd 100644 --- a/src/Signals.hpp +++ b/src/Signals.hpp @@ -26,9 +26,9 @@ #ifdef ZTS_ENABLE_CUSTOM_SIGNAL_HANDLERS -void _signal_handler(int signal); +void zts_signal_handler(int signal); -void _install_signal_handlers(); +void zts_install_signal_handlers(); #endif // ZTS_ENABLE_CUSTOM_SIGNAL_HANDLERS diff --git a/src/bindings/python/PythonSockets.cpp b/src/bindings/python/PythonSockets.cpp deleted file mode 100644 index 9692215..0000000 --- a/src/bindings/python/PythonSockets.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (c)2013-2021 ZeroTier, Inc. - * - * Use of this software is governed by the Business Source License included - * in the LICENSE.TXT file in the project's root directory. - * - * Change Date: 2025-01-01 - * - * On the date above, in accordance with the Business Source License, use - * of this software will be governed by version 2.0 of the Apache License. - */ -/****/ - -/** - * @file - * - * ZeroTier Socket API (Python) - */ - -#include - -#include "lwip/sockets.h" -#include "lwip/inet.h" - -#include "ZeroTierSockets.h" - -#ifdef ZTS_ENABLE_PYTHON - -int zts_py_setblocking(int fd, int block) -{ - int new_flags, cur_flags, err = 0; - - Py_BEGIN_ALLOW_THREADS - cur_flags = zts_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_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_fcntl(fd, F_GETFL, 0); - Py_END_ALLOW_THREADS - - if (flags < 0) { - return ZTS_ERR_SOCKET; - } - return flags & ZTS_O_NONBLOCK; -} - -static int zts_py_tuple_to_sockaddr(int family, - PyObject *addr_obj, struct zts_sockaddr *dst_addr, int *addrlen) -{ - if (family == AF_INET) { - struct zts_sockaddr_in* addr; - char *host_str; - int result, port; - if (!PyTuple_Check(addr_obj)) { - return ZTS_ERR_ARG; - } - if (!PyArg_ParseTuple(addr_obj, - "eti:zts_py_tuple_to_sockaddr", "idna", &host_str, &port)) { - return ZTS_ERR_ARG; - } - addr = (struct zts_sockaddr_in*)dst_addr; - zts_inet_pton(ZTS_AF_INET, host_str, &(addr->sin_addr.s_addr)); - PyMem_Free(host_str); - if (port < 0 || port > 0xFFFF) { - return ZTS_ERR_ARG; - } - if (result < 0) { - return ZTS_ERR_ARG; - } - addr->sin_family = AF_INET; - addr->sin_port = lwip_htons((short)port); - *addrlen = sizeof *addr; - return ZTS_ERR_OK; - } - if (family == AF_INET6) { - // TODO - } - return ZTS_ERR_ARG; -} - -PyObject * zts_py_accept(int fd) -{ - struct zts_sockaddr_in addrbuf; - socklen_t addrlen = sizeof(addrbuf); - memset(&addrbuf, 0, addrlen); - int err = zts_accept(fd, (struct zts_sockaddr*)&addrbuf, &addrlen); - char ipstr[ZTS_INET_ADDRSTRLEN]; - memset(ipstr, 0, sizeof(ipstr)); - zts_inet_ntop(ZTS_AF_INET, &(addrbuf.sin_addr), ipstr, ZTS_INET_ADDRSTRLEN); - PyObject *t; - t = PyTuple_New(3); - PyTuple_SetItem(t, 0, PyLong_FromLong(err)); // New file descriptor - PyTuple_SetItem(t, 1, PyUnicode_FromString(ipstr)); - PyTuple_SetItem(t, 2, PyLong_FromLong(lwip_ntohs(addrbuf.sin_port))); - Py_INCREF(t); - return t; -} - -int zts_py_listen(int fd, int backlog) -{ - if (backlog < 0) { - backlog = 128; - } - return zts_listen(fd, backlog); -} - -int zts_py_bind(int fd, int family, int type, PyObject *addr_obj) -{ - struct zts_sockaddr_storage addrbuf; - int addrlen; - int err; - if (zts_py_tuple_to_sockaddr(family, addr_obj, - (struct zts_sockaddr *)&addrbuf, &addrlen) != ZTS_ERR_OK) - { - return ZTS_ERR_ARG; - } - Py_BEGIN_ALLOW_THREADS - err = zts_bind(fd, (struct zts_sockaddr *)&addrbuf, addrlen); - Py_END_ALLOW_THREADS - return err; -} - -int zts_py_connect(int fd, int family, int type, PyObject *addr_obj) -{ - struct zts_sockaddr_storage addrbuf; - int addrlen; - int err; - if (zts_py_tuple_to_sockaddr(family, addr_obj, - (struct zts_sockaddr *)&addrbuf, &addrlen) != ZTS_ERR_OK) - { - return ZTS_ERR_ARG; - } - Py_BEGIN_ALLOW_THREADS - err = zts_connect(fd, (struct zts_sockaddr *)&addrbuf, addrlen); - Py_END_ALLOW_THREADS - return err; -} - -PyObject * zts_py_recv(int fd, int len, int flags) -{ - PyObject *t, *buf; - int bytes_read; - - buf = PyBytes_FromStringAndSize((char *) 0, len); - if (buf == NULL) { - return NULL; - } - - bytes_read = zts_recv(fd, PyBytes_AS_STRING(buf), len, flags); - t = PyTuple_New(2); - PyTuple_SetItem(t, 0, PyLong_FromLong(bytes_read)); - - if (bytes_read < 0) { - Py_DECREF(buf); - Py_INCREF(Py_None); - PyTuple_SetItem(t, 1, Py_None); - Py_INCREF(t); - return t; - } - - if (bytes_read != len) { - _PyBytes_Resize(&buf, bytes_read); - } - - PyTuple_SetItem(t, 1, buf); - Py_INCREF(t); - return t; -} - -int zts_py_send(int fd, PyObject *buf, int flags) -{ - Py_buffer output; - int bytes_sent; - - if (PyObject_GetBuffer(buf, &output, PyBUF_SIMPLE) != 0) { - return 0; - } - - bytes_sent = zts_send(fd, output.buf, output.len, flags); - PyBuffer_Release(&output); - - return bytes_sent; -} - -int zts_py_close(int fd) -{ - int err; - Py_BEGIN_ALLOW_THREADS - err = zts_close(fd); - Py_END_ALLOW_THREADS - return err; -} - -#endif // ZTS_ENABLE_PYTHON diff --git a/src/bindings/python/PythonSockets.cxx b/src/bindings/python/PythonSockets.cxx new file mode 100644 index 0000000..a4bd32e --- /dev/null +++ b/src/bindings/python/PythonSockets.cxx @@ -0,0 +1,203 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +/** + * @file + * + * ZeroTier Socket API (Python) + */ + +#include "ZeroTierSockets.h" +#include "lwip/inet.h" +#include "lwip/sockets.h" + +#include + +#ifdef ZTS_ENABLE_PYTHON + +int zts_py_setblocking(int fd, int block) +{ + int new_flags, cur_flags, err = 0; + + Py_BEGIN_ALLOW_THREADS cur_flags = zts_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_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_fcntl(fd, F_GETFL, 0); + Py_END_ALLOW_THREADS + + if (flags < 0) + { + return ZTS_ERR_SOCKET; + } + return flags & ZTS_O_NONBLOCK; +} + +static int zts_py_tuple_to_sockaddr(int family, PyObject* addr_obj, struct zts_sockaddr* dst_addr, int* addrlen) +{ + if (family == AF_INET) { + struct zts_sockaddr_in* addr; + char* host_str; + int result, port; + if (! PyTuple_Check(addr_obj)) { + return ZTS_ERR_ARG; + } + if (! PyArg_ParseTuple(addr_obj, "eti:zts_py_tuple_to_sockaddr", "idna", &host_str, &port)) { + return ZTS_ERR_ARG; + } + addr = (struct zts_sockaddr_in*)dst_addr; + zts_inet_pton(ZTS_AF_INET, host_str, &(addr->sin_addr.s_addr)); + PyMem_Free(host_str); + if (port < 0 || port > 0xFFFF) { + return ZTS_ERR_ARG; + } + if (result < 0) { + return ZTS_ERR_ARG; + } + addr->sin_family = AF_INET; + addr->sin_port = lwip_htons((short)port); + *addrlen = sizeof *addr; + return ZTS_ERR_OK; + } + if (family == AF_INET6) { + // TODO + } + return ZTS_ERR_ARG; +} + +PyObject* zts_py_accept(int fd) +{ + struct zts_sockaddr_in addrbuf = { 0 }; + socklen_t addrlen = sizeof(addrbuf); + int err = zts_accept(fd, (struct zts_sockaddr*)&addrbuf, &addrlen); + char ipstr[ZTS_INET_ADDRSTRLEN] = { 0 }; + zts_inet_ntop(ZTS_AF_INET, &(addrbuf.sin_addr), ipstr, ZTS_INET_ADDRSTRLEN); + PyObject* t; + t = PyTuple_New(3); + PyTuple_SetItem(t, 0, PyLong_FromLong(err)); // New file descriptor + PyTuple_SetItem(t, 1, PyUnicode_FromString(ipstr)); + PyTuple_SetItem(t, 2, PyLong_FromLong(lwip_ntohs(addrbuf.sin_port))); + Py_INCREF(t); + return t; +} + +int zts_py_listen(int fd, int backlog) +{ + if (backlog < 0) { + backlog = 128; + } + return zts_listen(fd, backlog); +} + +int zts_py_bind(int fd, int family, int type, PyObject* addr_obj) +{ + struct zts_sockaddr_storage addrbuf; + int addrlen; + int err; + if (zts_py_tuple_to_sockaddr(family, addr_obj, (struct zts_sockaddr*)&addrbuf, &addrlen) != ZTS_ERR_OK) { + return ZTS_ERR_ARG; + } + Py_BEGIN_ALLOW_THREADS err = zts_bind(fd, (struct zts_sockaddr*)&addrbuf, addrlen); + Py_END_ALLOW_THREADS return err; +} + +int zts_py_connect(int fd, int family, int type, PyObject* addr_obj) +{ + struct zts_sockaddr_storage addrbuf; + int addrlen; + int err; + if (zts_py_tuple_to_sockaddr(family, addr_obj, (struct zts_sockaddr*)&addrbuf, &addrlen) != ZTS_ERR_OK) { + return ZTS_ERR_ARG; + } + Py_BEGIN_ALLOW_THREADS err = zts_connect(fd, (struct zts_sockaddr*)&addrbuf, addrlen); + Py_END_ALLOW_THREADS return err; +} + +PyObject* zts_py_recv(int fd, int len, int flags) +{ + PyObject *t, *buf; + int bytes_read; + + buf = PyBytes_FromStringAndSize((char*)0, len); + if (buf == NULL) { + return NULL; + } + + bytes_read = zts_recv(fd, PyBytes_AS_STRING(buf), len, flags); + t = PyTuple_New(2); + PyTuple_SetItem(t, 0, PyLong_FromLong(bytes_read)); + + if (bytes_read < 0) { + Py_DECREF(buf); + Py_INCREF(Py_None); + PyTuple_SetItem(t, 1, Py_None); + Py_INCREF(t); + return t; + } + + if (bytes_read != len) { + _PyBytes_Resize(&buf, bytes_read); + } + + PyTuple_SetItem(t, 1, buf); + Py_INCREF(t); + return t; +} + +int zts_py_send(int fd, PyObject* buf, int flags) +{ + Py_buffer output; + int bytes_sent; + + if (PyObject_GetBuffer(buf, &output, PyBUF_SIMPLE) != 0) { + return 0; + } + + bytes_sent = zts_send(fd, output.buf, output.len, flags); + PyBuffer_Release(&output); + + return bytes_sent; +} + +int zts_py_close(int fd) +{ + int err; + Py_BEGIN_ALLOW_THREADS err = zts_close(fd); + Py_END_ALLOW_THREADS return err; +} + +#endif // ZTS_ENABLE_PYTHON diff --git a/src/bindings/python/node.py b/src/bindings/python/node.py new file mode 100644 index 0000000..2249af6 --- /dev/null +++ b/src/bindings/python/node.py @@ -0,0 +1,79 @@ +import libzt + +_user_specified_event_handler = None + + +class _EventCallbackClass(libzt.PythonDirectorCallbackClass): + """ZeroTier event callback class""" + + pass + + +class MyEventCallbackClass(_EventCallbackClass): + def on_zerotier_event(self, msg): + id = 0 + if msg.event_code == libzt.ZTS_EVENT_NODE_ONLINE: + id = msg.node.node_id + if msg.event_code == libzt.ZTS_EVENT_NODE_OFFLINE: + id = msg.node.node_id + if msg.event_code == libzt.ZTS_EVENT_NETWORK_READY_IP4: + id = msg.network.net_id + if msg.event_code == libzt.ZTS_EVENT_NETWORK_READY_IP6: + id = msg.network.net_id + if msg.event_code == libzt.ZTS_EVENT_PEER_DIRECT: + id = msg.peer.peer_id + if msg.event_code == libzt.ZTS_EVENT_PEER_RELAY: + id = msg.peer.peer_id + # Now that we've adjusted internal state, notify user + global _user_specified_event_handler + _user_specified_event_handler(msg.event_code, id) + + +class ZeroTierNode: + native_event_handler = None + + def __init__(self): + self.native_event_handler = MyEventCallbackClass() + libzt.zts_init_set_event_handler(self.native_event_handler) + + """ZeroTier Node""" + + def init_from_storage(self, storage_path): + """Initialize the node from storage (or tell it to write to that location)""" + return libzt.zts_init_from_storage(storage_path) + + """Set the node's event handler""" + + def init_set_event_handler(self, event_handler): + global _user_specified_event_handler + _user_specified_event_handler = event_handler + + def init_set_port(self, port): + """Set the node's primary port""" + return libzt.zts_init_set_port(port) + + def node_start(self): + """Start the ZeroTier service""" + return libzt.zts_node_start() + + def node_stop(self): + """Stop the ZeroTier service""" + return libzt.zts_node_stop() + + def node_free(self): + """Permanently shut down the network stack""" + return libzt.zts_node_free() + + def net_join(self, net_id): + """Join a ZeroTier network""" + return libzt.zts_net_join(net_id) + + def net_leave(self, net_id): + """Leave a ZeroTier network""" + return libzt.zts_net_leave(net_id) + + def node_is_online(self): + return libzt.zts_node_is_online() + + def net_transport_is_ready(self, net_id): + return libzt.zts_net_transport_is_ready(net_id) diff --git a/src/bindings/python/sockets.py b/src/bindings/python/sockets.py old mode 100644 new mode 100755 index 0f5b879..5d01947 --- a/src/bindings/python/sockets.py +++ b/src/bindings/python/sockets.py @@ -2,9 +2,6 @@ import libzt -class EventCallbackClass(libzt.PythonDirectorCallbackClass): - """ZeroTier event callback class""" - pass def handle_error(err): """Convert libzt error code to exception""" @@ -20,6 +17,7 @@ def handle_error(err): if err == libzt.ZTS_ERR_GENERAL: raise Exception("ZTS_ERR_GENERAL (" + str(err) + ")") + # This implementation of errno is NOT thread safe # That is, this value is shared among all lower-level socket calls # and may change for any reason at any time if you have multiple @@ -28,41 +26,10 @@ def errno(): """Return errno value of low-level socket layer""" return libzt.cvar.zts_errno -def start(path, callback, port): - """Start the ZeroTier service""" - libzt.zts_start(path, callback, port) - -def stop(): - """Stop the ZeroTier service""" - libzt.zts_stop() - -def restart(): - """[debug] Restarts the ZeroTier service and network stack""" - libzt.zts_restart() - -def free(): - """Permenantly shuts down the network stack""" - libzt.zts_free() - -def join(network_id): - """Join a ZeroTier network""" - libzt.zts_join(network_id) - -def leave(network_id): - """Leave a ZeroTier network""" - libzt.zts_leave(network_id) - -def zts_orbit(moon_world_id, moon_seed): - """Orbit a moon""" - return libzt.zts_orbit(moon_world_id, moon_seed) - -def zts_deorbit(moon_world_id): - """De-orbit a moon""" - return libzt.zts_deorbit(moon_world_id) - class socket: """Pythonic class that wraps low-level sockets""" + _fd = -1 # native layer file descriptor _family = -1 _type = -1 @@ -131,7 +98,9 @@ class socket: """libzt does not support this (yet)""" raise NotImplementedError("libzt does not support this (yet?)") - def getaddrinfo(self, host, port, sock_family=0, sock_type=0, sock_proto=0, flags=0): + def getaddrinfo( + self, host, port, sock_family=0, sock_type=0, sock_proto=0, flags=0 + ): """libzt does not support this (yet)""" raise NotImplementedError("libzt does not support this (yet?)") @@ -285,7 +254,8 @@ 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. OS File descriptors aren't used in libzt." + ) def dup(self): """libzt does not support this""" @@ -336,7 +306,7 @@ class socket: if backlog is not None and backlog < 0: backlog = 0 if backlog is None: - backlog = -1 # Lower-level code picks default + backlog = -1 # Lower-level code picks default err = libzt.zts_py_listen(self._fd, backlog) if err < 0: diff --git a/test/selftest.py b/test/selftest.py new file mode 100644 index 0000000..8236363 --- /dev/null +++ b/test/selftest.py @@ -0,0 +1,133 @@ +'''Example low-level socket usage''' + +import time +import sys + +import libzt + +def print_usage(): + '''print help''' + print( + "\nUsage: \n" + ) + print("Ex: python3 example.py server . 0123456789abcdef 8080") + print("Ex: python3 example.py client . 0123456789abcdef 192.168.22.1 8080\n") + if len(sys.argv) < 5: + print("Too few arguments") + if len(sys.argv) > 6: + print("Too many arguments") + sys.exit(0) + +# +# (Optional) Event handler +# +def on_zerotier_event(event_code, id): + if event_code == libzt.ZTS_EVENT_NODE_ONLINE: + print("ZTS_EVENT_NODE_ONLINE (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NODE_OFFLINE: + print("ZTS_EVENT_NODE_OFFLINE (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NETWORK_READY_IP4: + print("ZTS_EVENT_NETWORK_READY_IP4 (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_NETWORK_READY_IP6: + print("ZTS_EVENT_NETWORK_READY_IP6 (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_PEER_DIRECT: + print("ZTS_EVENT_PEER_DIRECT (" + str(event_code) + ") : " + hex(id)) + if event_code == libzt.ZTS_EVENT_PEER_RELAY: + print("ZTS_EVENT_PEER_RELAY (" + str(event_code) + ") : " + hex(id)) + +# +# Main +# +def main(): + mode = None # client|server + storage_path = "." # Where identity files are stored + net_id = 0 # Network to join + remote_ip = None # ZeroTier IP of remote node + remote_port = 8080 # ZeroTier port your app logic may use + + if len(sys.argv) < 5 or len(sys.argv) > 6: + print_usage() + if sys.argv[1] == "server" and len(sys.argv) == 5: + mode = sys.argv[1] + storage_path = sys.argv[2] + net_id = int(sys.argv[3], 16) + remote_port = int(sys.argv[4]) + if sys.argv[1] == "client" and len(sys.argv) == 6: + mode = sys.argv[1] + storage_path = sys.argv[2] + net_id = int(sys.argv[3], 16) + remote_ip = sys.argv[4] + remote_port = int(sys.argv[5]) + if mode is None: + print_usage() + print("mode = ", mode) + print("storage_path = ", storage_path) + print("net_id = ", net_id) + print("remote_ip = ", remote_ip) + print("remote_port = ", remote_port) + + # + # Node initialization and start + # + print("Starting ZeroTier...") + + n = libzt.ZeroTierNode() + n.init_set_event_handler(on_zerotier_event) # Optional + n.init_from_storage(storage_path) # Optional + n.init_set_port(9994) # Optional + n.node_start() + + print("Waiting for node to come online...") + while not n.node_is_online(): + time.sleep(1) + print("Joining network:", hex(net_id)) + n.net_join(net_id) + while not n.net_transport_is_ready(net_id): + time.sleep(1) + print("Joined network") + + # + # Example server + # + if mode == "server": + print("Starting server...") + serv = libzt.socket(libzt.ZTS_AF_INET, libzt.ZTS_SOCK_STREAM, 0) + try: + # serv.setblocking(True) + serv.bind(("0.0.0.0", remote_port)) + serv.listen(5) + conn, addr = serv.accept() + print("Accepted connection from: ", addr) + data = conn.recv(4096) + if data: + print("recv: ", data) + print("send: ", data) + sent_bytes = conn.send(data) # echo back to the server + print("sent: " + str(sent_bytes) + " byte(s)") + conn.close() + print("client disconnected") + except Exception as ex: + print(ex) + print("errno=", libzt.errno()) # See include/ZeroTierSockets.h for codes + + # + # Example client + # + if mode == "client": + print("Starting client...") + client = libzt.socket(libzt.ZTS_AF_INET, libzt.ZTS_SOCK_STREAM, 0) + try: + print("connecting...") + client.connect((remote_ip, remote_port)) + data = "Hello, world!" + print("send: ", data) + sent_bytes = client.send(data.encode('utf-8')) + print("sent: " + str(sent_bytes) + " byte(s)") + data = client.recv(1024) + print("recv: ", repr(data)) + except Exception as ex: + print(ex) + print("errno=", libzt.errno()) + +if __name__ == "__main__": + main()