From b60eadf76f92d7f8878fcbec6de7ab6ea4572a89 Mon Sep 17 00:00:00 2001 From: StevenWdV Date: Fri, 3 Dec 2021 12:31:30 +0100 Subject: Add initial Python wheel support --- wrappers/python/.gitignore | 4 ++ wrappers/python/Makefile | 16 +++-- wrappers/python/README.md | 25 +++++++- wrappers/python/discovery.py | 103 ------------------------------ wrappers/python/eduvpncommon/__init__.py | 0 wrappers/python/eduvpncommon/discovery.py | 85 ++++++++++++++++++++++++ wrappers/python/pyproject.toml | 6 ++ wrappers/python/setup.py | 48 ++++++++++++++ wrappers/python/test_discovery.py | 2 +- 9 files changed, 178 insertions(+), 111 deletions(-) create mode 100644 wrappers/python/.gitignore delete mode 100644 wrappers/python/discovery.py create mode 100644 wrappers/python/eduvpncommon/__init__.py create mode 100644 wrappers/python/eduvpncommon/discovery.py create mode 100644 wrappers/python/pyproject.toml create mode 100755 wrappers/python/setup.py (limited to 'wrappers/python') diff --git a/wrappers/python/.gitignore b/wrappers/python/.gitignore new file mode 100644 index 0000000..82f4fd9 --- /dev/null +++ b/wrappers/python/.gitignore @@ -0,0 +1,4 @@ +/build/ +/dist/ +*.egg-info/ +/eduvpncommon/lib/* diff --git a/wrappers/python/Makefile b/wrappers/python/Makefile index 690901f..3a73676 100644 --- a/wrappers/python/Makefile +++ b/wrappers/python/Makefile @@ -1,11 +1,17 @@ -.PHONY: compile test +.PHONY: build test clean -compile: - python3 -m discovery +ifdef PLAT_NAME +SETUP_ARGS += --plat-name=$(PLAT_NAME) +endif + +# Build for current platform only +build: + ./setup.py bdist_wheel $(SETUP_ARGS) test: - $(MAKE) -C ../../exports + $(MAKE) -C ../../exports copy-to COPY_TARGET=../wrappers/python/eduvpncommon/lib python3 -m unittest test_discovery + rm eduvpncommon/lib/* clean: - # Nothing to do + rm -rf build/ dist/ *.egg-info/ lib/* diff --git a/wrappers/python/README.md b/wrappers/python/README.md index fddcde6..b849e62 100644 --- a/wrappers/python/README.md +++ b/wrappers/python/README.md @@ -4,11 +4,32 @@ Python 3.6+ is assumed, but it may work with older versions. -## Test +TODO Build + +## Build & test First build the shared Go library. Next: -No dependencies, just reference `discovery.py` and call `verify`. +Build wheel using library for current platform: + +```shell +make +``` + +Build wheel using library for specified platform (passed to setuptools `--plat-name`): + +```shell +make PLAT_NAME=win32 +``` + +To install the wheel, run: + +```shell +pip install dist/eduvpncommon-[version]-py3-none-[platform].whl +``` + +You could also reference the discovery module directly and copy the library for the platform to the `eduvpncommon/lib` +folder. Test: diff --git a/wrappers/python/discovery.py b/wrappers/python/discovery.py deleted file mode 100644 index b22c82e..0000000 --- a/wrappers/python/discovery.py +++ /dev/null @@ -1,103 +0,0 @@ -import platform -from ctypes import * -from enum import Enum - -# TODO OpenBSD? - -_lib_prefixes = { - "windows": "", - "linux": "lib", - "darwin": "lib", -} - -_lib_suffixes = { - "windows": ".dll", - "linux": ".so", - "darwin": ".dylib", -} - -_arch = platform.machine().lower() -_arch = \ - { - "aarch64_be": "arm64", - "aarch64": "arm64", - "armv8b": "arm64", - "armv8l": "arm64", - "x86": "386", - "x86pc": "386", - "i86pc": "386", - "i386": "386", - "i686": "386", - "x86_64": "amd64", - "i686-64": "amd64", - }.get(_arch, _arch) - -_os = platform.system().lower() - -_lib = cdll.LoadLibrary(f"../../exports/{_os}/{_arch}/{_lib_prefixes[_os]}eduvpn_verify{_lib_suffixes[_os]}") - - -class GoSlice(Structure): - _fields_ = [("data", POINTER(c_char)), ("len", c_int64), ("cap", c_int64)] - - @staticmethod - def make(bs: bytes) -> "GoSlice": - return GoSlice((c_char * len(bs))(*bs), len(bs), len(bs)) - - -_lib.Verify.argtypes, _lib.Verify.restype = [GoSlice, GoSlice, GoSlice, c_uint64], c_int64 -_lib.InsecureTestingSetExtraKey.argtypes, _lib.InsecureTestingSetExtraKey.restype = [GoSlice], None - - -class VerifyErrorCode(Enum): - ErrUnknownExpectedFileName = 1 # Expected file name is not one of the recognized values. - ErrInvalidSignature = 2 # Signature is invalid (for the expected file type). - ErrInvalidSignatureUnknownKey = 3 # Signature was created with an unknown key and has not been verified. - ErrTooOld = 4 # Signature has a timestamp lower than the specified minimum signing time. - Unknown = -1 # Other unknown error - - -class VerifyError(Exception): - code: VerifyErrorCode - code_int: int # Original error code also for VerifyErrorCode.Unknown - - def __init__(self, err: int): - try: - self.code = VerifyErrorCode(err) - except ValueError: - self.code = VerifyErrorCode.Unknown - self.code_int = err - - def __str__(self): - return \ - { - VerifyErrorCode.ErrUnknownExpectedFileName: "unknown expected file name", - VerifyErrorCode.ErrInvalidSignature: "invalid signature", - VerifyErrorCode.ErrInvalidSignatureUnknownKey: "invalid signature (unknown key)", - VerifyErrorCode.ErrTooOld: "replay of previous signature (rollback)", - }[self.code] if self.code != VerifyErrorCode.Unknown else f"unknown verify error ({self.code_int})" - - -def verify(signature: bytes, signed_json: bytes, expected_file_name: str, min_sign_time: int) -> None: - """ - Verifies the signature on the JSON server_list.json/organization_list.json file. - If the function returns the signature is valid for the given file type. - - :param signature: .minisig signature file contents. - :param signed_json: Signed .json file contents. - :param expected_file_name: The file type to be verified, one of "server_list.json" or "organization_list.json". - :param min_sign_time: Minimum time for signature. Should be set to at least the time in a previously retrieved file. - - :raises VerifyException: If signature verification fails or expectedFileName is not one of the allowed values. - """ - - err = _lib.Verify(GoSlice.make(signature), GoSlice.make(signed_json), - GoSlice.make(expected_file_name.encode()), min_sign_time) - if err: - raise VerifyError(err) - - -def _insecure_testing_set_extra_key(key_string: str) -> None: - """Use for testing only, see Go documentation.""" - - _lib.InsecureTestingSetExtraKey(GoSlice.make(key_string.encode())) diff --git a/wrappers/python/eduvpncommon/__init__.py b/wrappers/python/eduvpncommon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wrappers/python/eduvpncommon/discovery.py b/wrappers/python/eduvpncommon/discovery.py new file mode 100644 index 0000000..ae5e2fe --- /dev/null +++ b/wrappers/python/eduvpncommon/discovery.py @@ -0,0 +1,85 @@ +import pathlib +import platform +from collections import defaultdict +from ctypes import * +from enum import Enum + +_lib_prefixes = defaultdict(lambda: "lib", { + "windows": "", +}) + +_lib_suffixes = defaultdict(lambda: ".so", { + "windows": ".dll", + "darwin": ".dylib", +}) + +_os = platform.system().lower() + +_libname = f"{_lib_prefixes[_os]}eduvpn_verify{_lib_suffixes[_os]}" +_lib = cdll.LoadLibrary(str(pathlib.Path(__file__).parent / "lib" / _libname)) + + +class GoSlice(Structure): + _fields_ = [("data", POINTER(c_char)), ("len", c_int64), ("cap", c_int64)] + + @staticmethod + def make(bs: bytes) -> "GoSlice": + return GoSlice((c_char * len(bs))(*bs), len(bs), len(bs)) + + +_lib.Verify.argtypes, _lib.Verify.restype = [GoSlice, GoSlice, GoSlice, c_uint64], c_int64 +_lib.InsecureTestingSetExtraKey.argtypes, _lib.InsecureTestingSetExtraKey.restype = [GoSlice], None + + +class VerifyErrorCode(Enum): + ErrUnknownExpectedFileName = 1 # Expected file name is not one of the recognized values. + ErrInvalidSignature = 2 # Signature is invalid (for the expected file type). + ErrInvalidSignatureUnknownKey = 3 # Signature was created with an unknown key and has not been verified. + ErrTooOld = 4 # Signature has a timestamp lower than the specified minimum signing time. + Unknown = -1 # Other unknown error + + +class VerifyError(Exception): + code: VerifyErrorCode + code_int: int # Original error code also for VerifyErrorCode.Unknown + + def __init__(self, err: int): + try: + self.code = VerifyErrorCode(err) + except ValueError: + self.code = VerifyErrorCode.Unknown + self.code_int = err + + def __str__(self): + return \ + { + VerifyErrorCode.ErrUnknownExpectedFileName: "unknown expected file name", + VerifyErrorCode.ErrInvalidSignature: "invalid signature", + VerifyErrorCode.ErrInvalidSignatureUnknownKey: "invalid signature (unknown key)", + VerifyErrorCode.ErrTooOld: "replay of previous signature (rollback)", + }[self.code] if self.code != VerifyErrorCode.Unknown else f"unknown verify error ({self.code_int})" + + +def verify(signature: bytes, signed_json: bytes, expected_file_name: str, min_sign_time: int) -> None: + """ + Verifies the signature on the JSON server_list.json/organization_list.json file. + If the function returns the signature is valid for the given file type. + + :param signature: .minisig signature file contents. + :param signed_json: Signed .json file contents. + :param expected_file_name: The file type to be verified, one of "server_list.json" or "organization_list.json". + :param min_sign_time: Minimum time for signature. Should be set to at least the time in a previously retrieved file. + + :raises VerifyException: If signature verification fails or expectedFileName is not one of the allowed values. + """ + + err = _lib.Verify(GoSlice.make(signature), GoSlice.make(signed_json), + GoSlice.make(expected_file_name.encode()), min_sign_time) + if err: + raise VerifyError(err) + + +def _insecure_testing_set_extra_key(key_string: str) -> None: + """Use for testing only, see Go documentation.""" + + _lib.InsecureTestingSetExtraKey(GoSlice.make(key_string.encode())) diff --git a/wrappers/python/pyproject.toml b/wrappers/python/pyproject.toml new file mode 100644 index 0000000..44836bb --- /dev/null +++ b/wrappers/python/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools", + "wheel", +] +build-backend = "setuptools.build_meta" diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py new file mode 100755 index 0000000..74f9266 --- /dev/null +++ b/wrappers/python/setup.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import os +import pathlib +import shutil +import sys + +from setuptools import setup +from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + +# You would say there would be a better way to do all of this, but I couldn't find it + +class bdist_wheel(_bdist_wheel): + def run(self): + self.plat_name_supplied = True # Force use platform + + libpath = { + # TODO probably partly incorrect + "win-amd64": "windows/amd64/eduvpn_verify.dll", + "win32": "windows/386/eduvpn_verify.dll", + "win-arm32": "windows/arm/eduvpn_verify.dll", + "win-arm64": "windows/arm64/eduvpn_verify.dll", + "linux_x86_64": "windows/amd64/eduvpn_verify.so", + "linux_i386": "windows/386/eduvpn_verify.so", + "linux_i686": "windows/386/eduvpn_verify.so", + "linux_arm": "windows/arm/eduvpn_verify.so", + "linux_aarch64": "windows/arm64/eduvpn_verify.so", + } + + if self.plat_name not in libpath: + print(f"Unknown platform: {self.plat_name}") + sys.exit(1) + + print(f"Building wheel for platform {self.plat_name}") + + shutil.copy2(f"../../exports/{libpath[self.plat_name]}", "eduvpncommon/lib/") + _bdist_wheel.run(self) + os.remove(f"eduvpncommon/lib/{pathlib.Path(libpath[self.plat_name]).name}") + + +setup( + name="eduvpncommon", + version="0.1.0", + packages=["eduvpncommon"], + python_requires=">=3.6", + package_data={"eduvpncommon": ["*eduvpn_verify*"]}, + cmdclass={"bdist_wheel": bdist_wheel}, +) diff --git a/wrappers/python/test_discovery.py b/wrappers/python/test_discovery.py index 74fd601..1282a3e 100755 --- a/wrappers/python/test_discovery.py +++ b/wrappers/python/test_discovery.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import unittest -import discovery +import eduvpncommon.discovery as discovery test_data_dir = "../../test_data" -- cgit v1.2.3