From c670303385d27cf5079f93cd713f29e26eaf5121 Mon Sep 17 00:00:00 2001 From: genotrance <> Date: Tue, 29 Mar 2022 20:40:24 +0000 Subject: [PATCH] Linux specific fixes --- HISTORY.txt | 5 +- README.md | 86 ++++++++------ px.py | 332 ++++++++++++++++++++++++++++------------------------ setup.py | 9 +- tools.py | 59 +++++++--- wproxy.py | 58 +++++---- 6 files changed, 316 insertions(+), 233 deletions(-) diff --git a/HISTORY.txt b/HISTORY.txt index 46b6cdf..a09cda7 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -1,9 +1,12 @@ -v0.6.0 - TBD +v0.6.0 - 2022-04-02 - Moved all Windows proxy detection code into wproxy.py, eventually to be made an independent module - Moved debugging code into separate debug.py module - Removed support for file:// PAC files configured in network settings since Windows 10 no longer supports this scheme (see https://bit.ly/3JKPgjR) +- Added support in wproxy to detect proxies defined via environment variables +- Added support for Linux - only NTLM and BASIC authentication are supported + for now, PAC files are not supported and Px is limited to a single process v0.5.1 - 2022-03-22 - Fixed #128 - IP:port split once from the right diff --git a/README.md b/README.md index 9e7fc0f..2c8eb1a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Chat on Gitter](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/genotrance/px) +[![Chat on Matrix](https://img.shields.io/matrix/genotrance_px:matrix.org)](https://matrix.to/#/#genotrance_px:matrix.org) # Px @@ -6,8 +7,10 @@ Px is a HTTP(s) proxy server that allows applications to authenticate through an NTLM or Kerberos proxy server, typically used in corporate deployments, without having to deal with the actual handshake. It is primarily designed to -run on Windows systems and authenticates on behalf of the application using the -currently logged in Windows user account. +run on Windows systems and automatically authenticates on using the currently +logged in Windows user account. It is also possible to run Px on Windows, Linux +and MacOS when single signon is not available by configuring the domain, username +and password to authenticate with. Px is similar to "NTLM Authorization Proxy Server" [NTLMAPS](http://ntlmaps.sourceforge.net/) and [Cntlm](http://cntlm.sourceforge.net/) in that it sits between the corporate @@ -17,7 +20,8 @@ requiring any user supplied credentials. This is accomplished by using Microsoft SSPI to generate the tokens and signatures required to authenticate with the proxy. Px also supports Kerberos and works with user supplied credentials for cases -where SSPI is not available. +where SSPI is not available. Kerberos/Negotiate support is only available on +Windows at this time. Microsoft provides a good starting point to understand how NTLM [authentication](https://msdn.microsoft.com/en-us/library/dd925287.aspx) works. And similarly for [Kerberos](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc772815(v=ws.10)) (warning: long!) @@ -80,29 +84,38 @@ needed to run standalone. Px requires only one piece of information in order to function - the server name and port of the proxy server. This needs to be configured in px.ini. If not -specified, Px will check Internet Options for any proxy definitions and use them. -Without this, Px will not work and exit immediately. +specified, Px will check Internet Options or environment variables for any proxy +definitions and use them. Without this, Px will try to connect to sites directly. The noproxy capability allows Px to connect to hosts in the configured subnets directly, bypassing the proxy altogether. This allows clients to connect to hosts within the intranet without requiring additional configuration for each -client or at the proxy. If noproxy is defined, the proxy is optional - this -allows Px to run as a regular proxy full time if required. +client or at the proxy. + +### Credentials If SSPI is not available or not preferred, providing a `username` in `domain\username` format allows Px to authenticate as that user. The corresponding password is -retrieved using Python keyring and needs to be setup directly in the backend. +retrieved using Python keyring and needs to be setup directly in the appropriate +OS specific backend. + +On Windows, Credential Manager is the backend and Px looks for a 'generic credential' +type with 'Px' as the network address name. Credential Manager and can be accessed +as follows: + + Control Panel > User Accounts > Credential Manager > Windows Credentials -On Windows, Credential Manager is the backend and can be accessed as follows: + Or on the command line: `rundll32.exe keymgr.dll, KRShowKeyMgr` - Control Panel > User Accounts > Credential Manager > Windows Credentials +On Linux, Gnome Keyring or KWallet can be used to store passwords. -Or run command line +If Python is available, credentials can be setup with: - rundll32.exe keymgr.dll, KRShowKeyMgr + `python -m keyring set Px username` -Px looks for a 'generic credential' type with 'Px' as the network address name. More -information on keyring backends can be found [here](https://pypi.org/project/keyring). +More information on keyring backends can be found [here](https://pypi.org/project/keyring). + +### Misc There are a few other settings to tweak in the INI file but most are obvious. All settings can be specified on the command line for convenience. The INI file @@ -146,11 +159,9 @@ Configuration: Specify config file. Valid file path, default: px.ini in working directory --proxy= --server= proxy:server= in INI file - Proxy server(s) to connect through. IP:port, hostname:port + NTLM server(s) to connect through. IP:port, hostname:port Multiple proxies can be specified comma separated. Px will iterate through - and use the one that works. Required field unless --noproxy is defined. If - remote server is not in noproxy list and proxy is undefined, Px will reject - the request + and use the one that works --pac= proxy:pac= PAC file to use to connect @@ -185,7 +196,7 @@ Configuration: --noproxy= proxy:noproxy= Direct connect to specific subnets like a regular proxy. Comma separated - Skip the proxy for connections to these subnets + Skip the NTLM proxy for connections to these subnets 127.0.0.1 - specific ip 192.168.0.* - wildcards 192.168.0.1-192.168.0.255 - ranges @@ -195,7 +206,7 @@ Configuration: Override or send User-Agent header on client's behalf --username= proxy:username= - Authentication to use when SSPI is unavailable. Format is domain\username + Authentication to use when SSPI is unavailable. Format is domain\\username Service name "Px" and this username are used to retrieve the password using Python keyring. Px only retrieves credentials and storage should be done directly in the keyring backend. @@ -224,9 +235,8 @@ Configuration: Timeout in seconds for connections before giving up. Valid float, default: 20 --proxyreload= settings:proxyreload= - Time interval in seconds before reloading proxy info. Valid int, default: 60 - Proxy info is reloaded from a PAC file found via WPAD or AutoConfig URL, or - manual proxy info defined in Internet Options + Time interval in seconds before refreshing proxy info. Valid int, default: 60 + Proxy info reloaded from manual proxy info defined in Internet Options --foreground settings:foreground= Run in foreground when frozen or with pythonw.exe. 0 or 1, default: 0 @@ -281,12 +291,11 @@ Workaround: Px doesn't have any GUI and runs completely in the background. It depends on the following Python packages: - `keyring`, `netaddr`, `ntlm-auth`, `psutil`, `pywin32`, `winkerberos` + `keyring`, `netaddr`, `ntlm-auth`, `psutil` - `futures` on Python 2.x + `pywin32`, `winkerberos` on Windows -Px is tested with the latest releases of Python 2.7, 3.5, 3.6 and 3.7 using the -Miniconda distribution. + `futures` on Python 2.x In order to make Px a capable proxy server, it is designed to run in multiple processes. The number of parallel workers or processes is configurable. However, @@ -295,25 +304,26 @@ sockets across processes in Windows. On older versions of Python, Px will run multi-threaded but in a single process. The number of threads per process is also configurable. -## Building - -To build a self-sufficient executable that does not depend on the presence of Python and -dependency modules, run `built.bat`. You will need [PyInstaller](https://www.pyinstaller.org) -and the Microsoft VC++ toolset. PyInstaller will prompt you with a link if not present. +On Linux, Px only runs in a single process. Further, only NTLM and BASIC auth +are supported and there is no PAC support. These limitations should be removed +over time. - `pip install pyinstaller` +While it should mostly work, Px is not tested on MacOSX since there's no test +environment available at this time to verify functionality. PRs are welcome to +help fix any issues. -If it complains about missing libraries, then you may modify build.bat to give it the path to the MS dlls: - - `pyinstaller --clean --paths "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64" --noupx -w -F -i px.ico px.py --hidden-import win32timezone --exclude-module win32ctypes` +## Building -Substitute the correct path for your system. +To build a self-sufficient executable that does not depend on the presence of +Python and dependency modules, both Nuitka and PyInstaller scripts are provided. +Run `python tools.py` for more details. ## Feedback Px is definitely a work in progress and any feedback or suggestions are welcome. It is hosted on [GitHub](https://github.com/genotrance/px) with an MIT license -so issues, forks and PRs are most appreciated. Also join us on [Gitter](https://gitter.im/genotrance/px) +so issues, forks and PRs are most appreciated. Join us on the [discussion](https://github.com/genotrance/px/discussions) +board, [Gitter](https://gitter.im/genotrance/px) or [Matrix](https://matrix.to/#/#genotrance_px:matrix.org) to chat about Px. ## Credits diff --git a/px.py b/px.py index fa08ae5..28909da 100755 --- a/px.py +++ b/px.py @@ -5,7 +5,6 @@ __version__ = "0.6.0" import base64 -import ctypes import multiprocessing import os import select @@ -17,20 +16,6 @@ import traceback from debug import pprint, Debug -import wproxy - -# Dependencies -try: - import concurrent.futures -except ImportError: - pprint("Requires module futures") - sys.exit() - -try: - import netaddr -except ImportError: - pprint("Requires module netaddr") - sys.exit() try: import psutil @@ -38,21 +23,32 @@ pprint("Requires module psutil") sys.exit() +# Python 2.x vs 3.x support try: - import sspi + import configparser + import http.server as httpserver + import socketserver + import urllib.parse as urlparse except ImportError: - pprint("Requires module pywin32 sspi") - sys.exit() + import ConfigParser as configparser + import SimpleHTTPServer as httpserver + import SocketServer as socketserver + import urlparse + + os.getppid = psutil.Process().ppid + PermissionError = OSError + +# Dependencies try: - import pywintypes + import concurrent.futures except ImportError: - pprint("Requires module pywin32 pywintypes") + pprint("Requires module futures") sys.exit() try: - import winkerberos + import netaddr except ImportError: - pprint("Requires module winkerberos") + pprint("Requires module netaddr") sys.exit() try: @@ -63,29 +59,45 @@ try: import keyring - import keyring.backends.Windows - keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring()) + # Explicit imports for Nuitka/PyInstaller + if sys.platform == "win32": + import keyring.backends.Windows + elif sys.platform.startswith("linux"): + import keyring.backends.SecretService + elif sys.platform == "darwin": + import keyring.backends.OS_X except ImportError: pprint("Requires module keyring") sys.exit() -# Python 2.x vs 3.x support -try: - import configparser - import http.server as httpserver - import socketserver - import urllib.parse as urlparse - import winreg -except ImportError: - import ConfigParser as configparser - import SimpleHTTPServer as httpserver - import SocketServer as socketserver - import urlparse - import _winreg as winreg +if sys.platform == "win32": + import ctypes - os.getppid = psutil.Process().ppid - PermissionError = WindowsError + # Python 2.x vs 3.x support + try: + import winreg + except ImportError: + import _winreg as winreg + + try: + import sspi + except ImportError: + pprint("Requires module pywin32 sspi") + sys.exit() + try: + import pywintypes + except ImportError: + pprint("Requires module pywin32 pywintypes") + sys.exit() + + try: + import winkerberos + except ImportError: + pprint("Requires module winkerberos") + sys.exit() + +import wproxy HELP = """Px v%s @@ -118,9 +130,7 @@ --proxy= --server= proxy:server= in INI file NTLM server(s) to connect through. IP:port, hostname:port Multiple proxies can be specified comma separated. Px will iterate through - and use the one that works. Required field unless --noproxy is defined. If - remote server is not in noproxy list and proxy is undefined, Px will reject - the request + and use the one that works --pac= proxy:pac= PAC file to use to connect @@ -195,8 +205,7 @@ --proxyreload= settings:proxyreload= Time interval in seconds before refreshing proxy info. Valid int, default: 60 - Proxy info reloaded from a PAC file found via WPAD or AutoConfig URL, or - manual proxy info defined in Internet Options + Proxy info reloaded from manual proxy info defined in Internet Options --foreground settings:foreground= Run in foreground when frozen or with pythonw.exe. 0 or 1, default: 0 @@ -222,8 +231,7 @@ class State(object): exit = False hostonly = False debug = None - noproxy = netaddr.IPSet([]) - noproxy_hosts = [] + noproxy = "" pac = "" wproxy = None proxy_refresh = None @@ -312,6 +320,8 @@ def b64encode(val): return base64.encodebytes(val) class AuthMessageGenerator: + get_response = None + def __init__(self, proxy_type, proxy_server_address): pwd = "" if State.username: @@ -322,9 +332,12 @@ def __init__(self, proxy_type, proxy_server_address): if proxy_type == "NTLM": if not pwd: - self.ctx = sspi.ClientAuth("NTLM", - os.environ.get("USERNAME"), scflags=0) - self.get_response = self.get_response_sspi + if sys.platform == "win32": + self.ctx = sspi.ClientAuth("NTLM", + os.environ.get("USERNAME"), scflags=0) + self.get_response = self.get_response_sspi + else: + dprint("No password configured for NTLM authentication") else: self.ctx = ntlm_auth.ntlm.NtlmContext( State.username, pwd, State.domain, "", ntlm_compatibility=3) @@ -349,13 +362,15 @@ def __init__(self, proxy_type, proxy_server_address): if any(char in State.username or char in pwd for char in illegal_control_characters): - dprint("Credentials contain invalid characters: %s" % ", ".join("0x" + "%x" % ord(char) for char in illegal_control_characters)) + dprint("Credentials contain invalid characters: %s" % + ", ".join("0x" + "%x" % ord(char) for char in illegal_control_characters)) else: # Remove newline appended by base64 function self.ctx = b64encode( "%s:%s" % (State.username, pwd))[:-1].decode() - self.get_response = self.get_response_basic - else: + self.get_response = self.get_response_basic + elif sys.platform == "win32": + # winkerberos only on Windows principal = None if pwd: if State.domain: @@ -369,6 +384,8 @@ def __init__(self, proxy_type, proxy_server_address): proxy_server_address, principal=principal, gssflags=0, mech_oid=winkerberos.GSS_MECH_OID_SPNEGO) self.get_response = self.get_response_wkb + else: + dprint("Unsupported proxy_type: " + proxy_type) def get_response_sspi(self, challenge=None): dprint("pywin32 SSPI") @@ -633,8 +650,12 @@ def do_proxy_type(self): if header[0].lower() == "proxy-authenticate": proxy_auth += header[1] + " " + # Limited support on Linux for now + supported = ["NTLM", "BASIC"] + if sys.platform == "win32": + supported.extend(["KERBEROS", "NEGOTIATE"]) for auth in proxy_auth.split(): - if auth.upper() in ["NTLM", "KERBEROS", "NEGOTIATE", "BASIC"]: + if auth.upper() in supported: proxy_type = auth break @@ -677,6 +698,8 @@ def do_transaction(self): # Generate auth message ntlm = AuthMessageGenerator(proxy_type, self.proxy_address[0]) + if ntlm.get_response == None: + return Response(503) ntlm_resp = ntlm.get_response() if ntlm_resp is None: dprint("Bad auth response") @@ -1066,9 +1089,10 @@ def print_banner(): multiprocessing.current_process().name) ) - if getattr(sys, "frozen", False) != False or "pythonw.exe" in sys.executable: - if State.config.getint("settings", "foreground") == 0: - detach_console() + if sys.platform == "win32": + if getattr(sys, "frozen", False) != False or "pythonw.exe" in sys.executable: + if State.config.getint("settings", "foreground") == 0: + detach_console() for section in State.config.sections(): for option in State.config.options(section): @@ -1155,7 +1179,7 @@ def reload_proxy(): return # Reload proxy information - State.wproxy = wproxy.Wproxy(noproxy = State.noproxy, debug_print = dprint) + State.wproxy = wproxy.Wproxy(debug_print = dprint) State.proxy_refresh = time.time() dprint("Proxy mode = " + str(State.wproxy.mode)) @@ -1173,7 +1197,7 @@ def parse_allow(allow): State.allow = wproxy.parse_noproxy(allow, iponly = True) def parse_noproxy(noproxy): - State.noproxy = wproxy.parse_noproxy(noproxy, iponly = True) + State.noproxy = noproxy def set_useragent(useragent): State.useragent = useragent @@ -1270,8 +1294,9 @@ def parse_config(): State.debug = Debug(dfile(), "w") dprint = State.debug.get_print() - if getattr(sys, "frozen", False) is not False or "pythonw.exe" in sys.executable: - attach_console() + if sys.platform == "win32": + if getattr(sys, "frozen", False) is not False or "pythonw.exe" in sys.executable: + attach_console() if "-h" in sys.argv or "--help" in sys.argv: pprint(HELP) @@ -1388,10 +1413,11 @@ def parse_config(): servers = wproxy.parse_proxy(State.config.get("proxy", "server")) - if "--install" in sys.argv: - install() - elif "--uninstall" in sys.argv: - uninstall() + if sys.platform == "win32": + if "--install" in sys.argv: + install() + elif "--uninstall" in sys.argv: + uninstall() elif "--quit" in sys.argv: quit() elif "--save" in sys.argv: @@ -1406,9 +1432,9 @@ def parse_config(): port = State.config.getint("proxy", "port") pac = "http://%s:%d/PxPACFile.pac" % (host, port) dprint("PAC URL is local: " + pac) - State.wproxy = wproxy.Wproxy(wproxy.MODE_CONFIG_PAC, [pac], State.noproxy, debug_print = dprint) + State.wproxy = wproxy.Wproxy(wproxy.MODE_CONFIG_PAC, [pac], debug_print = dprint) else: - State.wproxy = wproxy.Wproxy(noproxy = State.noproxy, debug_print = dprint) + State.wproxy = wproxy.Wproxy(debug_print = dprint) State.proxy_refresh = time.time() socket.setdefaulttimeout(State.config.getfloat("settings", "socktimeout")) @@ -1441,7 +1467,10 @@ def quit(force=False): if force: p.kill() else: - p.send_signal(signal.CTRL_C_EVENT) + if sys.platform == "win32": + p.send_signal(signal.CTRL_C_EVENT) + else: + p.send_signal(signal.SIGINT) except (psutil.AccessDenied, psutil.NoSuchProcess, PermissionError, SystemError): pass except: @@ -1491,106 +1520,107 @@ def get_script_path(): # Frozen mode return sys.executable -def get_script_cmd(): - spath = get_script_path() - if os.path.splitext(spath)[1].lower() == ".py": - return sys.executable + ' "%s"' % spath - - return spath +if sys.platform == "win32": + def get_script_cmd(): + spath = get_script_path() + if os.path.splitext(spath)[1].lower() == ".py": + return sys.executable + ' "%s"' % spath -def check_installed(): - ret = True - runkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ) - try: - winreg.QueryValueEx(runkey, "Px") - except: - ret = False - winreg.CloseKey(runkey) + return spath - return ret - -def install(): - if check_installed() is False: + def check_installed(): + ret = True runkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", 0, - winreg.KEY_WRITE) - winreg.SetValueEx(runkey, "Px", 0, winreg.REG_EXPAND_SZ, - get_script_cmd()) + r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ) + try: + winreg.QueryValueEx(runkey, "Px") + except: + ret = False winreg.CloseKey(runkey) - pprint("Px installed successfully") - else: - pprint("Px already installed") - sys.exit() + return ret + + def install(): + if check_installed() is False: + runkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Run", 0, + winreg.KEY_WRITE) + winreg.SetValueEx(runkey, "Px", 0, winreg.REG_EXPAND_SZ, + get_script_cmd()) + winreg.CloseKey(runkey) + pprint("Px installed successfully") + else: + pprint("Px already installed") -def uninstall(): - if check_installed() is True: - runkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", 0, - winreg.KEY_WRITE) - winreg.DeleteValue(runkey, "Px") - winreg.CloseKey(runkey) - pprint("Px uninstalled successfully") - else: - pprint("Px is not installed") + sys.exit() - sys.exit() + def uninstall(): + if check_installed() is True: + runkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Run", 0, + winreg.KEY_WRITE) + winreg.DeleteValue(runkey, "Px") + winreg.CloseKey(runkey) + pprint("Px uninstalled successfully") + else: + pprint("Px is not installed") -### -# Attach/detach console + sys.exit() -def attach_console(): - if ctypes.windll.kernel32.GetConsoleWindow() != 0: - dprint("Already attached to a console") - return + ### + # Attach/detach console - # Find parent cmd.exe if exists - pid = os.getpid() - while True: - try: - p = psutil.Process(pid) - except psutil.NoSuchProcess: - # No such parent - started without console - pid = -1 - break - - if os.path.basename(p.name()).lower() in [ - "cmd", "cmd.exe", "powershell", "powershell.exe"]: - # Found it - break - - # Search parent - pid = p.ppid() - - # Not found, started without console - if pid == -1: - dprint("No parent console to attach to") - return + def attach_console(): + if ctypes.windll.kernel32.GetConsoleWindow() != 0: + dprint("Already attached to a console") + return - dprint("Attaching to console " + str(pid)) - if ctypes.windll.kernel32.AttachConsole(pid) == 0: - dprint("Attach failed with error " + - str(ctypes.windll.kernel32.GetLastError())) - return + # Find parent cmd.exe if exists + pid = os.getpid() + while True: + try: + p = psutil.Process(pid) + except psutil.NoSuchProcess: + # No such parent - started without console + pid = -1 + break - if ctypes.windll.kernel32.GetConsoleWindow() == 0: - dprint("Not a console window") - return + if os.path.basename(p.name()).lower() in [ + "cmd", "cmd.exe", "powershell", "powershell.exe"]: + # Found it + break - reopen_stdout() + # Search parent + pid = p.ppid() -def detach_console(): - if ctypes.windll.kernel32.GetConsoleWindow() == 0: - return + # Not found, started without console + if pid == -1: + dprint("No parent console to attach to") + return - restore_stdout() + dprint("Attaching to console " + str(pid)) + if ctypes.windll.kernel32.AttachConsole(pid) == 0: + dprint("Attach failed with error " + + str(ctypes.windll.kernel32.GetLastError())) + return - if not ctypes.windll.kernel32.FreeConsole(): - dprint("Free console failed with error " + - str(ctypes.windll.kernel32.GetLastError())) - else: - dprint("Freed console successfully") + if ctypes.windll.kernel32.GetConsoleWindow() == 0: + dprint("Not a console window") + return + + reopen_stdout() + + def detach_console(): + if ctypes.windll.kernel32.GetConsoleWindow() == 0: + return + + restore_stdout() + + if not ctypes.windll.kernel32.FreeConsole(): + dprint("Free console failed with error " + + str(ctypes.windll.kernel32.GetLastError())) + else: + dprint("Freed console successfully") ### # Startup diff --git a/setup.py b/setup.py index e94f4b6..52aaecf 100644 --- a/setup.py +++ b/setup.py @@ -28,11 +28,15 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Win32 (MS Windows)", + "Environment :: MacOS X", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", + "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", @@ -41,6 +45,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Internet :: Proxy Servers" ], keywords = "proxy ntlm kerberos", @@ -51,8 +56,8 @@ "netaddr", "ntlm-auth", "psutil", - "pywin32", - "winkerberos" + 'pywin32;platform_system=="Windows"', + 'winkerberos;platform_system=="Windows"' ], data_files = [ ("lib/site-packages/px-proxy", [ diff --git a/tools.py b/tools.py index 8d7ab6b..041f50f 100644 --- a/tools.py +++ b/tools.py @@ -53,8 +53,7 @@ def remove(files): def wheel(): rmtree("__pycache__ build dist wheel") - os.system("python setup.py bdist_wheel --universal -p win32") - os.system("python setup.py bdist_wheel --universal -p win-amd64") + os.system(sys.executable + " setup.py bdist_wheel --universal") time.sleep(1) rmtree("__pycache__ build px_proxy.egg-info") @@ -74,24 +73,35 @@ def pyinstaller(): def nuitka(): rmtree("__pycache__ px.build px.dist") - os.system(sys.executable + " -m nuitka --standalone --include-module=win32timezone --nofollow-import-to=win32ctypes --prefer-source-code --remove-output px.py") + flags = "" + if sys.platform == "win32": + flags = "--include-module=win32timezone --nofollow-import-to=win32ctypes" + os.system(sys.executable + " -m nuitka --standalone %s --prefer-source-code --remove-output px.py" % flags) copy("px.ini HISTORY.txt LICENSE.txt README.md", "px.dist") time.sleep(1) os.chdir("px.dist") - if len(shutil.which("upx")) != 0: - os.system("upx --best px.exe python3*.dll") + if shutil.which("upx") is not None: + if sys.platform == "win32": + os.system("upx --best px.exe python3*.dll libcrypto*.dll") + else: + os.system("upx --best px") - remove("_asyncio.pyd _bz2.pyd _decimal.pyd _elementtree.pyd _hashlib.pyd _lzma.pyd _msi.pyd") - remove("_overlapped.pyd _queue.pyd _ssl.pyd _uuid.pyd _win32sysloader.pyd _zoneinfo.pyd pyexpat.pyd") - remove("libcrypto*.dll libssl*.dll pythoncom*.dll") + if sys.platform == "win32": + remove("_asyncio.pyd _bz2.pyd _decimal.pyd _elementtree.pyd _lzma.pyd _msi.pyd _overlapped.pyd ") + remove("pyexpat.pyd pythoncom*.dll _queue.pyd *ssl*.* _uuid.pyd _win32sysloader.pyd _zoneinfo.pyd") + else: + remove("_asyncio.so *bz2*.* *ctypes*.* *curses*.* _decimal.so libmpdec*.* *elementtree*.* *expat*.* ld-musl*.* *lzma*.* *ssl*.* libz*.* zlib.so") os.chdir("..") shutil.rmtree("__pycache__ px.build", True) - name = shutil.make_archive("px-v" + __version__, "zip", "px.dist") + arch = "gztar" + if sys.platform == "win32": + arch = "zip" + name = shutil.make_archive("px-v" + __version__, arch, "px.dist") time.sleep(1) shutil.move(name, "px.dist") @@ -261,20 +271,32 @@ def main(): # Setup if "--deps" in sys.argv: - os.system(sys.executable + " -m pip install keyring netaddr ntlm-auth psutil pywin32 winkerberos") + os.system(sys.executable + " -m pip install keyring netaddr ntlm-auth psutil") + if sys.platform == "win32": + os.system(sys.executable + " -m pip install pywin32 winkerberos") if "--devel" in sys.argv: - os.system(sys.executable + " -m pip install --upgrade build twine wheel nuitka pyinstaller") + if "--wheel" in sys.argv: + os.system(sys.executable + " -m pip install --upgrade build twine wheel") + + if sys.platform == "win32": + if "--pyinst" in sys.argv: + os.system(sys.executable + " -m pip install pyinstaller") - # Build - if "--wheel" in sys.argv: - wheel() + if "--nuitka" in sys.argv: + os.system(sys.executable + " -m pip install nuitka") - if "--pyinst" in sys.argv: - pyinstaller() + else: + # Build + if "--wheel" in sys.argv: + wheel() - if "--nuitka" in sys.argv: - nuitka() + if sys.platform == "win32": + if "--pyinst" in sys.argv: + pyinstaller() + + if "--nuitka" in sys.argv: + nuitka() # Delete if "--delete" in sys.argv: @@ -302,6 +324,7 @@ def main(): Setup: --deps Install all px runtime dependencies --devel Install development dependencies + --wheel --pyinst --nuitka Build: --wheel Build wheels for pypi.org diff --git a/wproxy.py b/wproxy.py index 13a1d24..d038ba2 100644 --- a/wproxy.py +++ b/wproxy.py @@ -3,9 +3,6 @@ import copy import socket import sys - -import netaddr - # Python 2.x vs 3.x support try: import urllib.parse as urlparse @@ -14,6 +11,12 @@ import urlparse import urllib as request +try: + import netaddr +except ImportError: + print("Requires module netaddr") + sys.exit() + # Proxy modes - source of proxy info MODE_NONE = 0 MODE_AUTO = 1 @@ -23,6 +26,11 @@ MODE_CONFIG = 5 MODE_CONFIG_PAC = 6 +MODES = [ + "MODE_NONE", "MODE_AUTO", "MODE_PAC", "MODE_MANUAL", + "MODE_ENV", "MODE_CONFIG", "MODE_CONFIG_PAC" +] + # Direct proxy connection DIRECT = ("DIRECT", 80) @@ -108,7 +116,7 @@ class _WproxyBase(object): noproxy = None noproxy_hosts = None - def __init__(self, debug_print = None): + def __init__(self, mode = MODE_NONE, servers = None, noproxy = None, debug_print = None): global dprint if debug_print is not None: dprint = debug_print @@ -117,13 +125,19 @@ def __init__(self, debug_print = None): self.noproxy = netaddr.IPSet([]) self.noproxy_hosts = [] - proxy = request.getproxies() - if "http" in proxy: - self.mode = MODE_ENV - self.servers = parse_proxy(proxy["http"]) + if mode != MODE_NONE: + # MODE_CONFIG or MODE_CONFIG_PAC + self.mode = mode + self.servers = servers or [] + noproxy, self.noproxy_hosts = parse_noproxy(noproxy) + else: + proxy = request.getproxies() + if "http" in proxy: + self.mode = MODE_ENV + self.servers = parse_proxy(proxy["http"]) - if "no" in proxy: - self.noproxy, self.noproxy_hosts = parse_noproxy(proxy["no"]) + if "no" in proxy: + self.noproxy, self.noproxy_hosts = parse_noproxy(proxy["no"]) def get_netloc(self, url): "Split url into netloc = hostname:port and path" @@ -207,8 +221,8 @@ def find_proxy_for_url(self, url): # Direct connection since in noproxy list return [DIRECT], ipport, path - if self.mode == MODE_ENV: - # Return proxy from the environment + if self.mode in [MODE_ENV, MODE_CONFIG]: + # Return proxy from environment or configuration return copy.deepcopy(self.servers), netloc, path return None, netloc, path @@ -279,7 +293,7 @@ class WINHTTP_PROXY_INFO(ctypes.Structure): class Wproxy(_WproxyBase): "Load proxy information from Windows Internet Options" - def __init__(self, mode = MODE_NONE, servers = None, noproxy = None, noproxy_hosts = None, debug_print = None): + def __init__(self, mode = MODE_NONE, servers = None, noproxy = None, debug_print = None): """ Load proxy information from Windows Internet Options Returns MODE_NONE, MODE_ENV, MODE_AUTO, MODE_PAC, MODE_MANUAL @@ -305,12 +319,10 @@ def __init__(self, mode = MODE_NONE, servers = None, noproxy = None, noproxy_hos self.noproxy_hosts = [] if mode != MODE_NONE: - # MODE_CONFIG or MODE_CONFIG_PAC - self.mode = mode - self.servers = servers or [] - self.noproxy = noproxy or netaddr.IPSet([]) - self.noproxy_hosts = noproxy_hosts or [] - else: + # Check MODE_CONFIG and MODE_CONFIG_PAC cases + super().__init__(mode, servers, noproxy, debug_print) + + if self.mode == MODE_NONE: # Get proxy info from Internet Options # MODE_AUTO, MODE_PAC or MODE_MANUAL ie_proxy_config = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG() @@ -351,7 +363,7 @@ def __init__(self, mode = MODE_NONE, servers = None, noproxy = None, noproxy_hos # Get from environment since nothing in Internet Options super().__init__() - dprint("Proxy mode = " + str(self.mode)) + dprint("Proxy mode =" + MODES[self.mode]) # Find proxy for specified URL using WinHttp API # Used internally for MODE_AUTO, MODE_PAC and MODE_CONFIG_PAC @@ -426,12 +438,12 @@ def find_proxy_for_url(self, url): servers, netloc, path = super().find_proxy_for_url(url) if servers is not None: - # MODE_NONE, url in no_proxy or MODE_ENV + # MODE_NONE, MODE_ENV, MODE_CONFIG or url in no_proxy return servers, netloc, path elif self.mode in [MODE_AUTO, MODE_PAC, MODE_CONFIG_PAC]: # Use proxies as resolved via WinHttp return parse_proxy(self.winhttp_find_proxy_for_url(url)), netloc, path - elif self.mode in [MODE_MANUAL, MODE_CONFIG]: + elif self.mode in [MODE_MANUAL]: # Use specific proxies configured return copy.deepcopy(self.servers), netloc, path else: @@ -443,4 +455,4 @@ class Wproxy(_WproxyBase): wp = Wproxy(debug_print=print) print("Servers: " + str(wp.servers)) print("Noproxy: " + str(wp.noproxy)) - print("Noproxy hosts: " + str(wp.noproxy_hosts)) \ No newline at end of file + print("Noproxy hosts: " + str(wp.noproxy_hosts))