From d6f75fae255a9f7f4d306c0cd5e37ea5dc7b1c6f Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 31 Dec 2021 13:48:03 -0500 Subject: [PATCH 001/275] Allow use of sudo or doas. This is an alternative solution to pull request #611. Previously, sshuttle would use doas on OpenBSD and sudo on Linux. However, some Linux distributions are opting to use doas. This patch changes the logic so that there can be multiple attempts to elevate privilages. If the first command fails to run, it moves on to the next command. Part of the existing code looked like it might be attempting to do this, but it didn't work. It also looks for the presence of doas and sudo in the path. If we can find doas (but cannot find sudo) or if the platform is OpenBSD, we try doas first. Otherwise, we try sudo, then doas. We try all the options until one succeeds (including running the command without sudo or doas) regardless of what is in the path. I'm open to adjusting the logic here based on feedback. If systems have both sudo and doas, they might be configured to give different users different permissions. For example, if a user wishes to use doas on this system, sshuttle would try sudo first and the user would need to enter invalid passwords to eventually cause sudo to fail and cause sshuttle to then try doas. This might not be ideal, but it avoids implement another sshuttle argument that the user would need to specify. Perhaps machines actually using doas will not have sudo installed? --- sshuttle/client.py | 132 ++++++++++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 43 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index f9bf04bc0..29e2b79ed 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -197,55 +197,101 @@ def __init__(self, method_name, sudo_pythonpath): if ssyslog._p: argvbase += ['--syslog'] - # Determine how to prefix the command in order to elevate privileges. - if platform.platform().startswith('OpenBSD'): - elev_prefix = ['doas'] # OpenBSD uses built in `doas` + # A list of commands that we can try to run to start the firewall. + argv_tries = [] + + if os.getuid() == 0: # No need to elevate privileges + argv_tries.append(argvbase) else: - elev_prefix = ['sudo', '-p', '[local sudo] Password: '] - - # Look for binary and switch to absolute path if we can find - # it. - path = which(elev_prefix[0]) - if path: - elev_prefix[0] = path - - if sudo_pythonpath: - elev_prefix += ['/usr/bin/env', - 'PYTHONPATH=%s' % - os.path.dirname(os.path.dirname(__file__))] - argv_tries = [elev_prefix + argvbase, argvbase] - - # we can't use stdin/stdout=subprocess.PIPE here, as we normally would, - # because stupid Linux 'su' requires that stdin be attached to a tty. - # Instead, attach a *bidirectional* socket to its stdout, and use - # that for talking in both directions. - (s1, s2) = socket.socketpair() - - def setup(): - # run in the child process - s2.close() - if os.getuid() == 0: - argv_tries = argv_tries[-1:] # last entry only + # Linux typically uses sudo; OpenBSD uses doas. However, some + # Linux distributions are starting to use doas. + sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']+argvbase + doas_cmd = ['doas']+argvbase + + # For clarity, try to replace executable name with the + # full path. + doas_path = which("doas") + if doas_path: + doas_cmd[0] = doas_path + sudo_path = which("sudo") + if sudo_path: + sudo_cmd[0] = sudo_path + + # sudo_pythonpath indicates if we should set the + # PYTHONPATH environment variable when elevating + # privileges. This can be adjusted with the + # --no-sudo-pythonpath option. + if sudo_pythonpath: + pp_prefix = ['/usr/bin/env', + 'PYTHONPATH=%s' % + os.path.dirname(os.path.dirname(__file__))] + sudo_cmd = pp_prefix + sudo_cmd + doas_cmd = pp_prefix + doas_cmd + + # If we can find doas and not sudo or if we are on + # OpenBSD, try using doas first. + if (doas_path and not sudo_path) or \ + platform.platform().startswith('OpenBSD'): + argv_tries = [doas_cmd, sudo_cmd, argvbase] + else: + argv_tries = [sudo_cmd, doas_cmd, argvbase] + + # Try all commands in argv_tries in order. If a command + # produces an error, try the next one. If command is + # successful, set 'success' variable and break. + success = False for argv in argv_tries: + # we can't use stdin/stdout=subprocess.PIPE here, as we + # normally would, because stupid Linux 'su' requires that + # stdin be attached to a tty. Instead, attach a + # *bidirectional* socket to its stdout, and use that for + # talking in both directions. + (s1, s2) = socket.socketpair() + + def setup(): + # run in the child process + s2.close() + try: - if argv[0] == 'su': - sys.stderr.write('[local su] ') + debug1("Starting firewall manager with command: %r" % argv) self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) # No env: Talking to `FirewallClient.start`, which has no i18n. - break except OSError as e: - log('Spawning firewall manager: %r' % argv) - raise Fatal(e) - self.argv = argv - s1.close() - self.pfile = s2.makefile('rwb') - line = self.pfile.readline() - self.check() - if line[0:5] != b'READY': - raise Fatal('%r expected READY, got %r' % (self.argv, line)) - method_name = line[6:-1] - self.method = get_method(method_name.decode("ASCII")) - self.method.set_firewall(self) + # This exception will occur if the program isn't + # present or isn't executable. + debug1('Unable to start firewall manager. Popen failed. ' + 'Command=%r Exception=%s' % (argv, e)) + continue + + self.argv = argv + s1.close() + self.pfile = s2.makefile('rwb') + line = self.pfile.readline() + + rv = self.p.poll() # Check if process is still running + if rv: + # We might get here if program runs and exits before + # outputting anything. For example, someone might have + # entered the wrong password to elevate privileges. + debug1('Unable to start firewall manager. ' + 'Process exited too early. ' + '%r returned %d' % (self.argv, rv)) + continue + + if line[0:5] != b'READY': + debug1('Unable to start firewall manager. ' + 'Expected READY, got %r. ' + 'Command=%r' % (line, self.argv)) + continue + + method_name = line[6:-1] + self.method = get_method(method_name.decode("ASCII")) + self.method.set_firewall(self) + success = True + break + + if not success: + raise Fatal("All attempts to elevate privileges failed.") def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, From e11db3980f4864ab44b0d4071f3894d5b325f183 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 31 Dec 2021 14:31:54 -0500 Subject: [PATCH 002/275] Minor improvement to tproxy documentation. Previously, tproxy was unique in its support of IPv6. Now, many sshuttle methods support IPv6 and tproxy remains the only option that supports UDP. --- docs/tproxy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tproxy.rst b/docs/tproxy.rst index c47cf78a6..3a54e63e0 100644 --- a/docs/tproxy.rst +++ b/docs/tproxy.rst @@ -1,6 +1,6 @@ TPROXY ====== -TPROXY is the only method that has full support of IPv6 and UDP. +TPROXY is the only method that supports UDP. There are some things you need to consider for TPROXY to work: From 175da40db71a2ace0aa341eec32004c166bc6d81 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 31 Dec 2021 14:54:17 -0500 Subject: [PATCH 003/275] Fix typo in tproxy documentation. "IPv6 DNS" was listed twice. --- docs/requirements.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.rst b/docs/requirements.rst index 8278b3180..821d52ffe 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -41,7 +41,7 @@ Supports: * IPv4 TCP * IPv4 UDP -* IPv6 DNS +* IPv4 DNS * IPv6 TCP * IPv6 UDP * IPv6 DNS From 44b772d049dd672d605ca008ecf60b650157f4cb Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 4 Jan 2022 11:29:34 +1100 Subject: [PATCH 004/275] Add readthedocs config --- .readthedocs.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..c08429841 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: requirements.txt + - method: setuptools + path: . From a2776cbbd9b7444e7377d78f0787d6c6e44c2077 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 4 Jan 2022 11:47:35 +1100 Subject: [PATCH 005/275] Upgrade Sphinx version --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index be5de1ff9..d35490d38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ setuptools-scm==6.3.2 +Sphinx==4.3.2 From 15a8752cc6e4b4c42f73c990315ea2d60a15661c Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Tue, 4 Jan 2022 14:39:15 -0500 Subject: [PATCH 006/275] Improve error message if tproxy method is used without running as root. When the tproxy method is used, sshuttle must be run as root: https://sshuttle.readthedocs.io/en/stable/tproxy.html Prior to this patch, sshuttle would encounter a exception and print a message about how a setsockopt() call had a "PermissionError: [Errno 1] Operation not permitted." With this patch, we catch this exception, print a more understandable error message, and exit. The lack of error message clarity caused at least one bug report: #136 --- sshuttle/methods/tproxy.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 0bd15a1bb..3450433aa 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -6,6 +6,7 @@ from sshuttle.helpers import debug1, debug2, debug3, Fatal, which import socket +import os IP_TRANSPARENT = 19 @@ -69,6 +70,15 @@ def recv_udp(self, udp_listener, bufsize): return None return srcip, dstip, data + def setsockopt_error(self, e): + """The tproxy method needs root permissions to successfully + set the IP_TRANSPARENT option on sockets. This method is + called when we receive a PermissionError when trying to do + so.""" + raise Fatal("Insufficient permissions for tproxy method.\n" + "Your effective UID is %d, not 0. Try rerunning as root.\n" + % os.geteuid()) + def send_udp(self, sock, srcip, dstip, data): if not srcip: debug1( @@ -77,16 +87,26 @@ def send_udp(self, sock, srcip, dstip, data): return sender = socket.socket(sock.family, socket.SOCK_DGRAM) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + try: + sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + except PermissionError as e: + self.setsockopt_error(e) sender.bind(srcip) sender.sendto(data, dstip) sender.close() def setup_tcp_listener(self, tcp_listener): - tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + try: + tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + except PermissionError as e: + self.setsockopt_error(e) def setup_udp_listener(self, udp_listener): - udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + try: + udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + except PermissionError as e: + self.setsockopt_error(e) + if udp_listener.v4 is not None: udp_listener.v4.setsockopt( socket.SOL_IP, IP_RECVORIGDSTADDR, 1) From 54b80e6ce2378659a13281ab867090b44af107da Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 7 Jan 2022 10:36:16 -0500 Subject: [PATCH 007/275] Fix defunct process after flushing DNS cache. When we flush the DNS cache by calling resolvectl, we should wait for the process to finish. This ensures that the cache is actually flushed and prevents the process from showing up as defunct when processes are listed. --- sshuttle/firewall.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index d3806cdcc..a519313aa 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -9,7 +9,7 @@ import sshuttle.ssyslog as ssyslog import sshuttle.helpers as helpers -from sshuttle.helpers import debug1, debug2, Fatal +from sshuttle.helpers import log, debug1, debug2, Fatal from sshuttle.methods import get_auto_method, get_method HOSTSFILE = '/etc/hosts' @@ -108,16 +108,24 @@ def flush_systemd_dns_cache(): # resolvectl in systemd 239. # https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816 + p = None if helpers.which("resolvectl"): debug2("Flushing systemd's DNS resolver cache: " "resolvectl flush-caches") - ssubprocess.Popen(["resolvectl", "flush-caches"], - stdout=ssubprocess.PIPE, env=helpers.get_env()) + p = ssubprocess.Popen(["resolvectl", "flush-caches"], + stdout=ssubprocess.PIPE, env=helpers.get_env()) elif helpers.which("systemd-resolve"): debug2("Flushing systemd's DNS resolver cache: " "systemd-resolve --flush-caches") - ssubprocess.Popen(["systemd-resolve", "--flush-caches"], - stdout=ssubprocess.PIPE, env=helpers.get_env()) + p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"], + stdout=ssubprocess.PIPE, env=helpers.get_env()) + + if p: + # Wait so flush is finished and process doesn't show up as defunct. + rv = p.wait() + if rv != 0: + log("Received non-zero return code %d when flushing DNS resolver " + "cache." % rv) # This is some voodoo for setting up the kernel's transparent From ae8af71886edd8f5cdcd812a839267ee973c3864 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 7 Jan 2022 11:35:37 -0500 Subject: [PATCH 008/275] Gracefully exit if firewall process receives Ctrl+C/SIGINT. Typically sshuttle exits by having the main sshuttle client process terminated. This closes file descriptors which the firewall process then sees and uses as a cue to cleanup the firewall rules. The firewall process ignored SIGINT/SIGTERM signals and used setsid() to prevent Ctrl+C from sending signals to the firewall process. This patch makes the firewall process accept SIGINT/SIGTERM signals and then in turn sends a SIGINT signal to the main sshuttle client process which then triggers a regular shutdown as described above. This allows a user to manually send a SIGINT/SIGTERM to either sshuttle process and have it exit gracefully. It also is needed if setsid() fails (known to occur if sudo's use_pty option is used) and then the Ctrl+C SIGINT signal goes to the firewall process. The PID of the sshuttle client process is sent to the firewall process. Using os.getppid() in the firewall process doesn't correctly return the sshuttle client PID. --- sshuttle/client.py | 4 ++-- sshuttle/firewall.py | 31 +++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index f9bf04bc0..abc38360d 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -298,8 +298,8 @@ def start(self): else: user = b'%d' % self.user - self.pfile.write(b'GO %d %s %s\n' % - (udp, user, bytes(self.tmark, 'ascii'))) + self.pfile.write(b'GO %d %s %s %d\n' % + (udp, user, bytes(self.tmark, 'ascii'), os.getpid())) self.pfile.flush() line = self.pfile.readline() diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index d3806cdcc..fb9471dc9 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -13,7 +13,7 @@ from sshuttle.methods import get_auto_method, get_method HOSTSFILE = '/etc/hosts' - +sshuttle_pid = None def rewrite_etc_hosts(hostmap, port): BAKFILE = '%s.sbak' % HOSTSFILE @@ -55,6 +55,23 @@ def restore_etc_hosts(hostmap, port): debug2('undoing /etc/hosts changes.') rewrite_etc_hosts({}, port) +def firewall_exit(signum, frame): + # The typical sshuttle exit is that the main sshuttle process + # exits, closes file descriptors it uses, and the firewall process + # notices that it can't read from stdin anymore and exits + # (cleaning up firewall rules). + # + # However, in some cases, Ctrl+C might get sent to the firewall + # process. This might caused if someone manually tries to kill the + # firewall process, or if sshuttle was started using sudo's use_pty option + # and they try to exit by pressing Ctrl+C. Here, we forward the + # Ctrl+C/SIGINT to the main sshuttle process which should trigger + # the typical exit process as described above. + global sshuttle_pid + if sshuttle_pid: + debug1("Relaying SIGINT to sshuttle process %d\n" % sshuttle_pid) + os.kill(sshuttle_pid, signal.SIGINT) + # Isolate function that needs to be replaced for tests def setup_daemon(): @@ -65,8 +82,8 @@ def setup_daemon(): # disappears; we still have to clean up. signal.signal(signal.SIGHUP, signal.SIG_IGN) signal.signal(signal.SIGPIPE, signal.SIG_IGN) - signal.signal(signal.SIGTERM, signal.SIG_IGN) - signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, firewall_exit) + signal.signal(signal.SIGINT, firewall_exit) # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, # I'll die automatically. @@ -230,12 +247,14 @@ def main(method_name, syslog): raise Fatal('expected GO but got %r' % line) _, _, args = line.partition(" ") - udp, user, tmark = args.strip().split(" ", 2) + global sshuttle_pid + udp, user, tmark, sshuttle_pid = args.strip().split(" ", 3) udp = bool(int(udp)) + sshuttle_pid = int(sshuttle_pid) if user == '-': user = None - debug2('Got udp: %r, user: %r, tmark: %s' % - (udp, user, tmark)) + debug2('Got udp: %r, user: %r, tmark: %s, sshuttle_pid: %d' % + (udp, user, tmark, sshuttle_pid)) subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] From 286bd3fa8058adc5fa475e4dd032ba54a029308c Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 7 Jan 2022 12:14:57 -0500 Subject: [PATCH 009/275] Make setsid() call in firewall process optional. We previously called setsid() to ensure that the SIGINT generated by Ctrl+C went to the main sshuttle process instead of the firewall process. With the previous commit, we gracefully shutdown if either the sshuttle process or firewall process receives a SIGINT. Therefore, the setsid() call is optional. We still try calling setsid() since the preferred shutdown process involves having the signal go to the main sshuttle process. However, setsid() will fail if the firewall process is started with sudo and sudo is configured with the use_pty option. --- sshuttle/firewall.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index fb9471dc9..28167e353 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -85,16 +85,17 @@ def setup_daemon(): signal.signal(signal.SIGTERM, firewall_exit) signal.signal(signal.SIGINT, firewall_exit) - # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, - # I'll die automatically. + # Calling setsid() here isn't strictly necessary. However, it forces + # Ctrl+C to get sent to the main sshuttle process instead of to + # the firewall process---which is our preferred way to shutdown. + # Nonetheless, if the firewall process receives a SIGTERM/SIGINT + # signal, it will relay a SIGINT to the main sshuttle process + # automatically. try: os.setsid() except OSError: - raise Fatal("setsid() failed. This may occur if you are using sudo's " - "use_pty option. sshuttle does not currently work with " - "this option. An imperfect workaround: Run the sshuttle " - "command with sudo instead of running it as a regular " - "user and entering the sudo password when prompted.") + # setsid() fails if sudo is configured with the use_pty option. + pass # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we From 8e826cfa7d64f5baa60c074d63e9bfe7076c5cd3 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 7 Jan 2022 13:05:42 -0500 Subject: [PATCH 010/275] Print to console with \r\n line endings. If we run sudo with the use_pty option, the firewall process is started in a new pseudoterminal. Other processes that are still printing to the terminal (i.e., the main sshuttle client process, messages from the shuttle server) have their output incorreclty displayed. A newline character simply moves the output to the next line without returning the cursor to the beginning of the line. Simply changing all print commands to use \r\n line endings fixes the problem and does not appear to cause any trouble in other configurations. --- sshuttle/assembler.py | 2 +- sshuttle/helpers.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 3cffdee97..6eb88002d 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -18,7 +18,7 @@ name = name.decode("ASCII") nbytes = int(sys.stdin.readline()) if verbosity >= 2: - sys.stderr.write(' s: assembling %r (%d bytes)\n' + sys.stderr.write(' s: assembling %r (%d bytes)\r\n' % (name, nbytes)) content = z.decompress(sys.stdin.read(nbytes)) diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 372feb32a..8ff536a4d 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -18,15 +18,19 @@ def log(s): # Put newline at end of string if line doesn't have one. if not s.endswith("\n"): s = s+"\n" - # Allow multi-line messages - if s.find("\n") != -1: - prefix = logprefix - s = s.rstrip("\n") - for line in s.split("\n"): - sys.stderr.write(prefix + line + "\n") - prefix = " " - else: - sys.stderr.write(logprefix + s) + + prefix = logprefix + s = s.rstrip("\n") + for line in s.split("\n"): + # We output with \r\n instead of \n because when we use + # sudo with the use_pty option, the firewall process, the + # other processes printing to the terminal will have the + # \n move to the next line, but they will fail to reset + # cursor to the beginning of the line. Printing output + # with \r\n endings fixes that problem and does not appear + # to cause problems elsewhere. + sys.stderr.write(prefix + line + "\r\n") + prefix = " " sys.stderr.flush() except IOError: # this could happen if stderr gets forcibly disconnected, eg. because From 80a822e07921b0d1325bec0b28f4001550ebddd2 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 7 Jan 2022 13:21:16 -0500 Subject: [PATCH 011/275] Fix flake8 and unit test errors introduced by use_pty fixes. --- sshuttle/firewall.py | 2 ++ tests/client/test_firewall.py | 2 +- tests/client/test_helpers.py | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 28167e353..84b249daa 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -15,6 +15,7 @@ HOSTSFILE = '/etc/hosts' sshuttle_pid = None + def rewrite_etc_hosts(hostmap, port): BAKFILE = '%s.sbak' % HOSTSFILE APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port @@ -55,6 +56,7 @@ def restore_etc_hosts(hostmap, port): debug2('undoing /etc/hosts changes.') rewrite_etc_hosts({}, port) + def firewall_exit(signum, frame): # The typical sshuttle exit is that the main sshuttle process # exits, closes file descriptors it uses, and the firewall process diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index d249361ee..76aac8935 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -15,7 +15,7 @@ def setup_daemon(): {inet},1.2.3.33 {inet6},2404:6800:4004:80c::33 PORTS 1024,1025,1026,1027 -GO 1 - 0x01 +GO 1 - 0x01 12345 HOST 1.2.3.3,existing """.format(inet=AF_INET, inet6=AF_INET6)) stdout = Mock() diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py index 45e7ea516..8e69d45d1 100644 --- a/tests/client/test_helpers.py +++ b/tests/client/test_helpers.py @@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\n'), + call.write('prefix: message\r\n'), call.flush(), - call.write('prefix: abc\n'), + call.write('prefix: abc\r\n'), call.flush(), - call.write('prefix: message 1\n'), + call.write('prefix: message 1\r\n'), call.flush(), - call.write('prefix: message 2\n'), - call.write(' line2\n'), - call.write(' line3\n'), + call.write('prefix: message 2\r\n'), + call.write(' line2\r\n'), + call.write(' line3\r\n'), call.flush(), - call.write('prefix: message 3\n'), - call.write(' line2\n'), - call.write(' line3\n'), + call.write('prefix: message 3\r\n'), + call.write(' line2\r\n'), + call.write(' line3\r\n'), call.flush(), ] @@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\n'), + call.write('prefix: message\r\n'), call.flush(), ] @@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\n'), + call.write('prefix: message\r\n'), call.flush(), ] @@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\n'), + call.write('prefix: message\r\n'), call.flush(), ] From 9429f387eaebfd43dc87016e014a4939ce9fb7db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jan 2022 10:12:20 +0000 Subject: [PATCH 012/275] Bump setuptools-scm from 6.3.2 to 6.4.0 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.3.2 to 6.4.0. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v6.3.2...v6.4.0) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d35490d38..108a5c082 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==6.3.2 +setuptools-scm==6.4.0 Sphinx==4.3.2 From 0890ebd383487c532e8bebcae35c107cdabcd5eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jan 2022 10:12:35 +0000 Subject: [PATCH 013/275] Bump setuptools-scm from 6.4.0 to 6.4.1 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.0 to 6.4.1. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v6.4.0...v6.4.1) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 108a5c082..d549972c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==6.4.0 +setuptools-scm==6.4.1 Sphinx==4.3.2 From be667c78547b35279c77b86a86a0dd331f34ae3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jan 2022 10:08:39 +0000 Subject: [PATCH 014/275] Bump setuptools-scm from 6.4.1 to 6.4.2 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v6.4.1...v6.4.2) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d549972c9..563436f26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==6.4.1 +setuptools-scm==6.4.2 Sphinx==4.3.2 From ecc2d68a0621c229b28427d98a665aacce3c16e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jan 2022 20:09:06 +0000 Subject: [PATCH 015/275] Bump sphinx from 4.3.2 to 4.4.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.4.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.2...v4.4.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 563436f26..b806d499b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==4.3.2 +Sphinx==4.4.0 From d23a0fd2c5ac8fa0c3112ce9e1c492932c62eba0 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 22 Jan 2022 09:44:59 +1100 Subject: [PATCH 016/275] Revert "Bump sphinx from 4.3.2 to 4.4.0" --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b806d499b..563436f26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==4.4.0 +Sphinx==4.3.2 From 3d51bcba95a89b30d593a38b124b10278d2393c7 Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 28 Jan 2022 09:27:47 +1100 Subject: [PATCH 017/275] Move release notes to github --- CHANGES.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2bd289b82..f8d6afc9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,12 +1,9 @@ ========== Change log ========== -All notable changes to this project will be documented in this file. The format -is based on `Keep a Changelog`_ and this project -adheres to `Semantic Versioning`_. +Release notes now moved to https://github.com/sshuttle/sshuttle/releases/ -.. _`Keep a Changelog`: http://keepachangelog.com/ -.. _`Semantic Versioning`: http://semver.org/ +These are the old release notes. 1.0.5 - 2020-12-29 From 0f92735ee596b15283154c95060a6b5ae7156f95 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Thu, 3 Feb 2022 13:42:35 -0500 Subject: [PATCH 018/275] Make --sudoers option work properly, fix regression in v1.1.0 Commit d6f75fa unintentionally changed the order of some of the parameters when running the firewall process. This prevented the --sudoers option from working properly. This patch restores the previous ordering. Most discussion was in issue #724. Also fixes #722 and #723. --- sshuttle/client.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 100d2ba3c..83b96c701 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -205,8 +205,8 @@ def __init__(self, method_name, sudo_pythonpath): else: # Linux typically uses sudo; OpenBSD uses doas. However, some # Linux distributions are starting to use doas. - sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']+argvbase - doas_cmd = ['doas']+argvbase + sudo_cmd = ['sudo', '-p', '[local sudo] Password: '] + doas_cmd = ['doas'] # For clarity, try to replace executable name with the # full path. @@ -225,8 +225,13 @@ def __init__(self, method_name, sudo_pythonpath): pp_prefix = ['/usr/bin/env', 'PYTHONPATH=%s' % os.path.dirname(os.path.dirname(__file__))] - sudo_cmd = pp_prefix + sudo_cmd - doas_cmd = pp_prefix + doas_cmd + sudo_cmd = sudo_cmd + pp_prefix + doas_cmd = doas_cmd + pp_prefix + + # Final order should be: sudo/doas command, env + # pythonpath, and then argvbase (sshuttle command). + sudo_cmd = sudo_cmd + argvbase + doas_cmd = doas_cmd + argvbase # If we can find doas and not sudo or if we are on # OpenBSD, try using doas first. From 0c3b615736a008cfeea43575a5403075c783b5b3 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 4 Feb 2022 11:22:56 -0500 Subject: [PATCH 019/275] Improve message when bind fails with a IPv6 address The comments at the end of issue #673 shows an example where sshuttle exits with an OSError exception when it cannot bind to an IPv6 address. This patch makes a suggestion to try the --disable-ipv6 option instead of the cryptic error message. --- sshuttle/client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 83b96c701..51af6a0a1 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -164,7 +164,22 @@ def bind(self, address_v6, address_v4): self.bind_called = True if address_v6 is not None: self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto) - self.v6.bind(address_v6) + try: + self.v6.bind(address_v6) + except OSError as e: + if e.errno == errno.EADDRNOTAVAIL: + # On an IPv6 Linux machine, this situation occurs + # if you run the following prior to running + # sshuttle: + # + # echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6 + # echo 1 > /proc/sys/net/ipv6/conf/default/disable_ipv6 + raise Fatal("Could not bind to an IPv6 socket with " + "address %s and port %s. " + "Potential workaround: Run sshuttle " + "with '--disable-ipv6'." + % (str(address_v6[0]), str(address_v6[1]))) + raise e else: self.v6 = None if address_v4 is not None: From 09c534bcf3befe4feb89b13089c3490326c1d83b Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 4 Feb 2022 15:27:48 -0500 Subject: [PATCH 020/275] Clarify --disable-ipv6 in man page. The description for --disable-ipv6 did not list all methods that support IPv6. --- docs/manpage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 39e166bae..9445c8cf8 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -242,8 +242,8 @@ Options .. option:: --disable-ipv6 - Disable IPv6 support for methods that support it (nft, tproxy, and - pf). + Disable IPv6 support for methods that support it (nat, nft, + tproxy, and pf). .. option:: --firewall From 166e4d6742f7242ab38520b3c7c7a79593f2b212 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 10:12:58 +0000 Subject: [PATCH 021/275] Bump actions/setup-python from 2.3.1 to 2.3.2 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.3.1...v2.3.2) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6001397c8..ddc7b5786 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v2.4.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From d378cbd5828ab1e13d3d17b372f3512b021a85c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 10:13:04 +0000 Subject: [PATCH 022/275] Bump pytest from 6.2.5 to 7.0.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index b59009e8a..5fae51194 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==6.2.5 +pytest==7.0.0 pytest-cov==3.0.0 flake8==4.0.1 pyflakes==2.4.0 From 04214eaf89001fe35eb29137228cbefaf30f43d5 Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:56:48 +0900 Subject: [PATCH 023/275] test hosts with no port specified --- tests/client/test_options.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 6f86a8a7f..1b5f4a71c 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -105,3 +105,11 @@ def test_parse_subnetport_ip6_with_mask_and_port(): def test_convert_arg_line_to_args_skips_comments(): parser = sshuttle.options.MyArgumentParser() assert parser.convert_arg_line_to_args("# whatever something") == [] + + +def test_parse_subnetport_host(): + assert set(sshuttle.options.parse_subnetport('example.com')) \ + == set([ + (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 0, 0), + (socket.AF_INET, '93.184.216.34', 32, 0, 0), + ]) From 2f026c84af9b471c407261756b3ec7c32f203282 Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:56:58 +0900 Subject: [PATCH 024/275] test hosts with port specified --- tests/client/test_options.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 1b5f4a71c..c36721380 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -113,3 +113,16 @@ def test_parse_subnetport_host(): (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 0, 0), (socket.AF_INET, '93.184.216.34', 32, 0, 0), ]) + + +def test_parse_subnetport_host_with_port(): + assert set(sshuttle.options.parse_subnetport('example.com:80')) \ + == set([ + (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 80), + (socket.AF_INET, '93.184.216.34', 32, 80, 80), + ]) + assert set(sshuttle.options.parse_subnetport('example.com:80-90')) \ + == set([ + (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 90), + (socket.AF_INET, '93.184.216.34', 32, 80, 90), + ]) From 19e2a1810d40dc5b2dd1596d9cb805480455ed16 Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:24:03 +0900 Subject: [PATCH 025/275] add getaddrinfo mock for test-cases with hosts --- tests/client/test_options.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index c36721380..91e28713b 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -27,6 +27,15 @@ _ip6_swidths = (48, 64, 96, 115, 128) +def _mock_getaddrinfo(host, *_): + return { + "example.com": [ + (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('2606:2800:220:1:248:1893:25c8:1946', 0, 0, 0)), + (socket.AF_INET, socket.SOCK_STREAM, 0, '', ('93.184.216.34', 0)), + ], + }.get(host, []) + + def test_parse_subnetport_ip4(): for ip_repr, ip in _ip4_reprs.items(): assert sshuttle.options.parse_subnetport(ip_repr) \ From e5eb5afef063aa7ffaf5098000ab3435f8c8591d Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:25:42 +0900 Subject: [PATCH 026/275] use mocked getaddrinfo to make host name resolution stable --- tests/client/test_options.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 91e28713b..5e045b866 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -1,5 +1,6 @@ import socket from argparse import ArgumentTypeError as Fatal +from unittest.mock import patch import pytest @@ -116,7 +117,8 @@ def test_convert_arg_line_to_args_skips_comments(): assert parser.convert_arg_line_to_args("# whatever something") == [] -def test_parse_subnetport_host(): +@patch('sshuttle.options.socket.getaddrinfo', side_effect = _mock_getaddrinfo) +def test_parse_subnetport_host(mock_getaddrinfo): assert set(sshuttle.options.parse_subnetport('example.com')) \ == set([ (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 0, 0), @@ -124,7 +126,8 @@ def test_parse_subnetport_host(): ]) -def test_parse_subnetport_host_with_port(): +@patch('sshuttle.options.socket.getaddrinfo', side_effect = _mock_getaddrinfo) +def test_parse_subnetport_host_with_port(mock_getaddrinfo): assert set(sshuttle.options.parse_subnetport('example.com:80')) \ == set([ (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 80), From b9b89c3f55f9088408c89010e15e52a81d4ec52e Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:26:51 +0900 Subject: [PATCH 027/275] add another example for host resolution tests --- tests/client/test_options.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 5e045b866..85dc88ddd 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -34,6 +34,10 @@ def _mock_getaddrinfo(host, *_): (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('2606:2800:220:1:248:1893:25c8:1946', 0, 0, 0)), (socket.AF_INET, socket.SOCK_STREAM, 0, '', ('93.184.216.34', 0)), ], + "my.local": [ + (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 0, 0, 0)), + (socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 0)), + ], }.get(host, []) @@ -124,6 +128,11 @@ def test_parse_subnetport_host(mock_getaddrinfo): (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 0, 0), (socket.AF_INET, '93.184.216.34', 32, 0, 0), ]) + assert set(sshuttle.options.parse_subnetport('my.local')) \ + == set([ + (socket.AF_INET6, '::1', 128, 0, 0), + (socket.AF_INET, '127.0.0.1', 32, 0, 0), + ]) @patch('sshuttle.options.socket.getaddrinfo', side_effect = _mock_getaddrinfo) @@ -138,3 +147,13 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo): (socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 90), (socket.AF_INET, '93.184.216.34', 32, 80, 90), ]) + assert set(sshuttle.options.parse_subnetport('my.local:445')) \ + == set([ + (socket.AF_INET6, '::1', 128, 445, 445), + (socket.AF_INET, '127.0.0.1', 32, 445, 445), + ]) + assert set(sshuttle.options.parse_subnetport('my.local:445-450')) \ + == set([ + (socket.AF_INET6, '::1', 128, 445, 450), + (socket.AF_INET, '127.0.0.1', 32, 445, 450), + ]) From 1d4c059f445aab6a42fe29306baba5795f2567b7 Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Thu, 10 Feb 2022 08:36:14 +0900 Subject: [PATCH 028/275] format styles: E251 unexpected spaces around keyword / parameter equals (flake8) --- tests/client/test_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 85dc88ddd..6969ce15e 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -121,7 +121,7 @@ def test_convert_arg_line_to_args_skips_comments(): assert parser.convert_arg_line_to_args("# whatever something") == [] -@patch('sshuttle.options.socket.getaddrinfo', side_effect = _mock_getaddrinfo) +@patch('sshuttle.options.socket.getaddrinfo', side_effect=_mock_getaddrinfo) def test_parse_subnetport_host(mock_getaddrinfo): assert set(sshuttle.options.parse_subnetport('example.com')) \ == set([ @@ -135,7 +135,7 @@ def test_parse_subnetport_host(mock_getaddrinfo): ]) -@patch('sshuttle.options.socket.getaddrinfo', side_effect = _mock_getaddrinfo) +@patch('sshuttle.options.socket.getaddrinfo', side_effect=_mock_getaddrinfo) def test_parse_subnetport_host_with_port(mock_getaddrinfo): assert set(sshuttle.options.parse_subnetport('example.com:80')) \ == set([ From 2f5c946b488cfffba713949d432c488c8c0b148f Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Thu, 10 Feb 2022 08:40:14 +0900 Subject: [PATCH 029/275] define flake8 max line length longer (79 to 128) --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 8b398f06b..dbdf1616a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ identity=0x1784577F811F6EAC count=true show-source=true statistics=true +max-line-length=128 [tool:pytest] addopts = --cov=sshuttle --cov-branch --cov-report=term-missing From 48ab82b81efd177cb982283675ee897d3ae8a186 Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Wed, 9 Feb 2022 22:04:51 +0900 Subject: [PATCH 030/275] test a wildcarded host acceptable --- tests/client/test_options.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 6969ce15e..fe0411703 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -38,6 +38,10 @@ def _mock_getaddrinfo(host, *_): (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 0, 0, 0)), (socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 0)), ], + "*.blogspot.com": [ + (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('2404:6800:4004:821::2001', 0, 0, 0)), + (socket.AF_INET, socket.SOCK_STREAM, 0, '', ('142.251.42.129', 0)), + ], }.get(host, []) @@ -133,6 +137,11 @@ def test_parse_subnetport_host(mock_getaddrinfo): (socket.AF_INET6, '::1', 128, 0, 0), (socket.AF_INET, '127.0.0.1', 32, 0, 0), ]) + assert set(sshuttle.options.parse_subnetport('*.blogspot.com')) \ + == set([ + (socket.AF_INET6, '2404:6800:4004:821::2001', 128, 0, 0), + (socket.AF_INET, '142.251.42.129', 32, 0, 0), + ]) @patch('sshuttle.options.socket.getaddrinfo', side_effect=_mock_getaddrinfo) @@ -157,3 +166,13 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo): (socket.AF_INET6, '::1', 128, 445, 450), (socket.AF_INET, '127.0.0.1', 32, 445, 450), ]) + assert set(sshuttle.options.parse_subnetport('*.blogspot.com:80')) \ + == set([ + (socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 80), + (socket.AF_INET, '142.251.42.129', 32, 80, 80), + ]) + assert set(sshuttle.options.parse_subnetport('*.blogspot.com:80-90')) \ + == set([ + (socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90), + (socket.AF_INET, '142.251.42.129', 32, 80, 90), + ]) From 016919cf955165380c8188357ffacaa855c0239d Mon Sep 17 00:00:00 2001 From: mangano-ito <47137677+mangano-ito@users.noreply.github.com> Date: Wed, 9 Feb 2022 22:09:34 +0900 Subject: [PATCH 031/275] accept a wildcarded host --- sshuttle/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sshuttle/options.py b/sshuttle/options.py index a0a06c30d..7cb05d169 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -37,9 +37,9 @@ def parse_subnetport_file(s): def parse_subnetport(s): if s.count(':') > 1: - rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$' + rx = r'(?:\[?(?:\*\.)?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$' else: - rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$' + rx = r'((?:\*\.)?[\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$' m = re.match(rx, s) if not m: From f4150b7283014978b92a432df0c91fc8372de453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:13:35 +0000 Subject: [PATCH 032/275] Bump pytest from 7.0.0 to 7.0.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.0.0...7.0.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5fae51194..4c6a8c860 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.0.0 +pytest==7.0.1 pytest-cov==3.0.0 flake8==4.0.1 pyflakes==2.4.0 From d9d3533b82c43aa3bed6019e03dbd6ee23cc6f98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:10:14 +0000 Subject: [PATCH 033/275] Bump actions/setup-python from 2.3.2 to 3 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.3.2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index ddc7b5786..743d44bfa 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v2.4.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 1ed09fbe72cf105f69172f8d08c14faa706284ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:10:13 +0000 Subject: [PATCH 034/275] Bump actions/checkout from 2.4.0 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 743d44bfa..c4e9ab41f 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,7 +20,7 @@ jobs: python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: From 9431bb7a2f45067858bb73f83c69eac81d448f9c Mon Sep 17 00:00:00 2001 From: lbausch Date: Thu, 24 Feb 2022 10:54:28 +0100 Subject: [PATCH 035/275] Fix typo --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index bf1dfc2b0..3711d0491 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -81,7 +81,7 @@ ability to run sshuttle:: DO NOT run this command with sudo, it will ask for your sudo password when it is needed. -A costume user or group can be set with the : +A custom user or group can be set with the : option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid values for this vary based on how your system is configured. Values such as usernames, groups pre-pended with `%` and sudoers user aliases will work. See From 5719d424de732d218ade4670fd0d987b8aad2736 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 4 Mar 2022 12:15:24 -0500 Subject: [PATCH 036/275] Remove --sudoers, improve --sudoers-no-modify Allowing sshuttle to add/overwrite sudoers configuration file at locations of the users' choosing adds complexity to the code compared to asking users to install the sudo configuration themselves. It requires sshuttle to make decisions about how much effort we put into ensuring that the file is written to a proper location. The current method relies on the 'realpath' program which is not installed on MacOS by default. There are serious problems when the sudo configuration is used to allow a user to *only* run sshuttle as root (with or without a password). First, that user could then use the --sudoers option to give other users sudo privileges. Second, the user can run any command as root because sshuttle accepts a --ssh-cmd parameter which allows a user to specify a program that sshuttle should run. There may also be additional issues that we have not identified. By removing the --sudoers option (and the associated sudoers-add script), this reduces the problems above. This code keeps the --sudoers-no-modify feature which prints a configuration to stdout for the user to install. It includes a clear warning about how --ssh-cmd could potentially be abused to run other programs. A warning about some of these issues has been in sshuttle since version 1.1.0. This commit also adds that warning to more locations in the documentation. --- bin/sudoers-add | 84 ------------------------------------------- docs/installation.rst | 3 +- docs/manpage.rst | 29 +++++++-------- docs/usage.rst | 49 ++++++++----------------- setup.py | 1 - sshuttle/cmdline.py | 18 ++-------- sshuttle/options.py | 24 ++++--------- sshuttle/sudoers.py | 75 +++++++++++++------------------------- 8 files changed, 61 insertions(+), 222 deletions(-) delete mode 100755 bin/sudoers-add diff --git a/bin/sudoers-add b/bin/sudoers-add deleted file mode 100755 index e359d46eb..000000000 --- a/bin/sudoers-add +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# William Mantly -# MIT License -# https://github.com/wmantly/sudoers-add - -NEWLINE=$'\n' -CONTENT="" -ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" - -if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then - echo "Usage: $ME [file_path] [sudoers-file-name]" - echo "Usage: [content] | $ME sudoers-file-name" - echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}" - echo "The config can come from a file, first usage example or piped in second example." - - exit 0 -fi - -if [ "$1" == "" ]; then - (>&2 echo "This command take at lest one argument. See $ME --help") - - exit 1 -fi - -if [ "$2" == "" ]; then - FILE_NAME=$1 - shift -else - FILE_NAME=$2 -fi - -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root" - - exit 1 -fi - -while read -r line -do - CONTENT+="${line}${NEWLINE}" -done < "${1:-/dev/stdin}" - -if [ "$CONTENT" == "" ]; then - (>&2 echo "No config content specified. See $ME --help") - exit 1 -fi - -if [ "$FILE_NAME" == "" ]; then - (>&2 echo "No sudoers file name specified. See $ME --help") - exit 1 -fi - -# Verify that the resulting file name begins with /etc/sudoers.d -FILE_NAME="$(realpath "/etc/sudoers.d/$FILE_NAME")" -if [[ "$FILE_NAME" != "/etc/sudoers.d/"* ]] ; then - echo -n "Invalid sudoers filename: Final sudoers file " - echo "location ($FILE_NAME) does not begin with /etc/sudoers.d" - exit 1 -fi - -# Make a temp file to hold the sudoers config -umask 077 -TEMP_FILE=$(mktemp) -echo "$CONTENT" > "$TEMP_FILE" - -# Make sure the content is valid -visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1) -visudo_code=$? -# The temp file is no longer needed -rm "$TEMP_FILE" - -if [ $visudo_code -eq 0 ]; then - echo "$CONTENT" > "$FILE_NAME" - chmod 0440 "$FILE_NAME" - echo "The sudoers file $FILE_NAME has been successfully created!" - - exit 0 -else - echo "Invalid sudoers config!" - echo "$visudo_STDOUT" - - exit 1 -fi - diff --git a/docs/installation.rst b/docs/installation.rst index 4dc18f31e..97c62a8e2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -19,6 +19,5 @@ Installation Optionally after installation ----------------------------- -- Add to sudoers file:: +- Install sudoers configuration. For details, see the "Sudoers File" section in :doc:`usage` - sshuttle --sudoers diff --git a/docs/manpage.rst b/docs/manpage.rst index 9445c8cf8..6ea75f3cb 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -262,28 +262,23 @@ Options makes it a lot easier to debug and test the :option:`--auto-hosts` feature. -.. option:: --sudoers - - sshuttle will auto generate the proper sudoers.d config file and add it. - Once this is completed, sshuttle will exit and tell the user if - it succeed or not. Do not call this options with sudo, it may generate a - incorrect config file. - .. option:: --sudoers-no-modify - sshuttle will auto generate the proper sudoers.d config and print it to - stdout. The option will not modify the system at all. + sshuttle prints a configuration to stdout which allows a user to + run sshuttle without a password. This option is INSECURE because, + with some cleverness, it also allows the user to run any command + as root without a password. The output also includes a suggested + method for you to install the configuration. -.. option:: --sudoers-user + Use --sudoers-user to modify the user that it applies to. - Set the user name or group with %group_name for passwordless operation. - Default is the current user.set ALL for all users. Only works with - --sudoers or --sudoers-no-modify option. - -.. option:: --sudoers-filename +.. option:: --sudoers-user - Set the file name for the sudoers.d file to be added. Default is - "sshuttle_auto". Only works with --sudoers. + Set the user name or group with %group_name for passwordless + operation. Default is the current user. Set to ALL for all users + (NOT RECOMMENDED: See note about security in --sudoers-no-modify + documentation above). Only works with the --sudoers-no-modify + option. .. option:: -t , --tmark= diff --git a/docs/usage.rst b/docs/usage.rst index 3711d0491..912d84695 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -71,44 +71,23 @@ admin access on the server. Sudoers File ------------ -sshuttle can auto-generate the proper sudoers.d file using the current user -for Linux and OSX. Doing this will allow sshuttle to run without asking for -the local sudo password and to give users who do not have sudo access -ability to run sshuttle:: - sshuttle --sudoers +sshuttle can generate a sudoers.d file for Linux and MacOS. This +allows one or more users to run sshuttle without entering the +local sudo password. **WARNING:** This option is *insecure* +because, with some cleverness, it also allows these users to run any +command (via the --ssh-cmd option) as root without a password. -DO NOT run this command with sudo, it will ask for your sudo password when -it is needed. - -A custom user or group can be set with the : -option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid -values for this vary based on how your system is configured. Values such as -usernames, groups pre-pended with `%` and sudoers user aliases will work. See -the sudoers manual for more information on valid user specif actions. -The options must be used with `--sudoers`:: - - sshuttle --sudoers --sudoers-user mike - sshuttle --sudoers --sudoers-user %sudo - -The name of the file to be added to sudoers.d can be configured as well. This -is mostly not necessary but can be useful for giving more than one user -access to sshuttle. The default is `sshuttle_auto`:: - - sshuttle --sudoer --sudoers-filename sshuttle_auto_mike - sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy - -You can also see what configuration will be added to your system without -modifying anything. This can be helpful if the auto feature does not work, or -you want more control. This option also works with `--sudoers-username`. -`--sudoers-filename` has no effect with this option:: +To print a sudo configuration file and see a suggested way to install it, run:: sshuttle --sudoers-no-modify -This will simply sprint the generated configuration to STDOUT. Example:: - - 08:40 PM william$ sshuttle --sudoers-no-modify - - Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall +A custom user or group can be set with the +:option:`sshuttle --sudoers-no-modify --sudoers-user {user_descriptor}` +option. Valid values for this vary based on how your system is configured. +Values such as usernames, groups pre-pended with `%` and sudoers user +aliases will work. See the sudoers manual for more information on valid +user specif actions. The option must be used with `--sudoers-no-modify`:: - william ALL=NOPASSWD: SSHUTTLE304 + sshuttle --sudoers-no-modify --sudoers-user mike + sshuttle --sudoers-no-modify --sudoers-user %sudo diff --git a/setup.py b/setup.py index 54b751dc1..df3032f12 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,6 @@ def version_scheme(version): "Programming Language :: Python :: 3.9", "Topic :: System :: Networking", ], - scripts=['bin/sudoers-add'], entry_points={ 'console_scripts': [ 'sshuttle = sshuttle.cmdline:main', diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index 2295d366b..c619cd157 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -1,6 +1,5 @@ import re import socket -import platform import sshuttle.helpers as helpers import sshuttle.client as client import sshuttle.firewall as firewall @@ -14,20 +13,9 @@ def main(): opt = parser.parse_args() - if opt.sudoers or opt.sudoers_no_modify: - if platform.platform().startswith('OpenBSD'): - log('Automatic sudoers does not work on BSD') - return 1 - - if not opt.sudoers_filename: - log('--sudoers-file must be set or omitted.') - return 1 - - sudoers( - user_name=opt.sudoers_user, - no_modify=opt.sudoers_no_modify, - file_name=opt.sudoers_filename - ) + if opt.sudoers_no_modify: + # sudoers() calls exit() when it completes + sudoers(user_name=opt.sudoers_user) if opt.daemon: opt.syslog = 1 diff --git a/sshuttle/options.py b/sshuttle/options.py index 7cb05d169..0ac7690db 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -396,18 +396,15 @@ def convert_arg_line_to_args(self, arg_line): (internal use only) """ ) -parser.add_argument( - "--sudoers", - action="http://github.com/store_true", - help=""" - Add sshuttle to the sudoers for this user - """ -) parser.add_argument( "--sudoers-no-modify", action="http://github.com/store_true", help=""" - Prints the sudoers config to STDOUT and DOES NOT modify anything. + Prints a sudo configuration to STDOUT which allows a user to + run sshuttle without a password. This option is INSECURE because, + with some cleverness, it also allows the user to run any command + as root without a password. The output also includes a suggested + method for you to install the configuration. """ ) parser.add_argument( @@ -415,16 +412,7 @@ def convert_arg_line_to_args(self, arg_line): default="", help=""" Set the user name or group with %%group_name for passwordless operation. - Default is the current user.set ALL for all users. Only works with - --sudoers or --sudoers-no-modify option. - """ -) -parser.add_argument( - "--sudoers-filename", - default="sshuttle_auto", - help=""" - Set the file name for the sudoers.d file to be added. Default is - "sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option. + Default is the current user. Only works with the --sudoers-no-modify option. """ ) parser.add_argument( diff --git a/sshuttle/sudoers.py b/sshuttle/sudoers.py index ea675784e..dece49a18 100644 --- a/sshuttle/sudoers.py +++ b/sshuttle/sudoers.py @@ -2,70 +2,45 @@ import sys import getpass from uuid import uuid4 -from subprocess import Popen, PIPE -from sshuttle.helpers import log, debug1 -from distutils import spawn -path_to_sshuttle = sys.argv[0] -path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9] -# randomize command alias to avoid collisions -command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()} +def build_config(user_name): + template = ''' +# WARNING: If you intend to restrict a user to only running the +# sshuttle command as root, THIS CONFIGURATION IS INSECURE. +# When a user can run sshuttle as root (with or without a password), +# they can also run other commands as root because sshuttle itself +# can run a command specified by the user with the --ssh-cmd option. + +# INSTRUCTIONS: Add this text to your sudo configuration to run +# sshuttle without needing to enter a sudo password. To use this +# configuration, run 'visudo /etc/sudoers.d/sshuttle_auto' as root and +# paste this text into the editor that it opens. If you want to give +# multiple users these privilages, you may wish to use use different +# filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john). + +# This configuration was initially generated by the +# 'sshuttle --sudoers-no-modify' command. -# Template for the sudoers file -template = ''' Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s * %(user_name)s ALL=NOPASSWD: %(ca)s ''' -warning_msg = "# WARNING: When you allow a user to run sshuttle as root,\n" \ - "# they can then use sshuttle's --ssh-cmd option to run any\n" \ - "# command as root.\n" - - -def build_config(user_name): - content = warning_msg - content += template % { - 'ca': command_alias, - 'dist_packages': path_to_dist_packages, + content = template % { + # randomize command alias to avoid collisions + 'ca': 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}, + 'dist_packages': os.path.dirname(os.path.abspath(__file__))[:-9], 'py': sys.executable, - 'path': path_to_sshuttle, + 'path': sys.argv[0], 'user_name': user_name, } return content -def save_config(content, file_name): - process = Popen([ - '/usr/bin/sudo', - spawn.find_executable('sudoers-add'), - file_name, - ], stdout=PIPE, stdin=PIPE) - - process.stdin.write(content.encode()) - - streamdata = process.communicate()[0] - sys.stdout.write(streamdata.decode("ASCII")) - returncode = process.returncode - - if returncode: - log('Failed updating sudoers file.') - debug1(streamdata) - exit(returncode) - else: - log('Success, sudoers file update.') - exit(0) - - -def sudoers(user_name=None, no_modify=None, file_name=None): +def sudoers(user_name=None): user_name = user_name or getpass.getuser() content = build_config(user_name) - - if no_modify: - sys.stdout.write(content) - exit(0) - else: - sys.stdout.write(warning_msg) - save_config(content, file_name) + sys.stdout.write(content) + exit(0) From 6d36916f48ae69abe175682f111f505923aba339 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 29 Mar 2022 11:44:35 +1100 Subject: [PATCH 037/275] Remove support for Python 3.6 and 3.7 Fixes #716 --- .github/workflows/pythonpackage.yml | 2 +- docs/requirements.rst | 4 ++-- setup.py | 5 ++--- tox.ini | 6 ++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c4e9ab41f..8b05efbf8 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v3 diff --git a/docs/requirements.rst b/docs/requirements.rst index 821d52ffe..9a2e1867a 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -6,7 +6,7 @@ Client side Requirements - sudo, or root access on your client machine. (The server doesn't need admin access.) -- Python 3.6 or greater. +- Python 3.8 or greater. Linux with NAT method @@ -72,7 +72,7 @@ cmd.exe with Administrator access. See :doc:`windows` for more information. Server side Requirements ------------------------ -- Python 3.6 or greater. +- Python 3.8 or greater. Additional Suggested Software diff --git a/setup.py b/setup.py index df3032f12..74c9238e2 100755 --- a/setup.py +++ b/setup.py @@ -49,10 +49,9 @@ def version_scheme(version): "License :: OSI Approved :: " "GNU Lesser General Public License v2 or later (LGPLv2+)", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: System :: Networking", ], entry_points={ @@ -60,7 +59,7 @@ def version_scheme(version): 'sshuttle = sshuttle.cmdline:main', ], }, - python_requires='>=3.6', + python_requires='>=3.8', install_requires=[ ], tests_require=[ diff --git a/tox.ini b/tox.ini index 84ed81de7..b63d87c93 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,15 @@ [tox] downloadcache = {toxworkdir}/cache/ envlist = - py36, - py37, py38, py39, + py310, [testenv] basepython = - py36: python3.6 - py37: python3.7 py38: python3.8 py39: python3.9 + py310: python3.10 commands = pip install -e . # actual flake8 test From a7ca6d47a6f975d894bd1af3f69d1b104a8157df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:49:05 +0000 Subject: [PATCH 038/275] Bump pytest from 7.0.1 to 7.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...7.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 4c6a8c860..664ea4b26 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.0.1 +pytest==7.1.1 pytest-cov==3.0.0 flake8==4.0.1 pyflakes==2.4.0 From 6f92bd8ccff16f73f5c3d63a6de96d0f4d95a4b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:49:28 +0000 Subject: [PATCH 039/275] Bump sphinx from 4.3.2 to 4.5.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.5.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.2...v4.5.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 563436f26..d5a8119f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==4.3.2 +Sphinx==4.5.0 From 10341f3ad6eb3ff62a2aab5519aa156f74cbfb1d Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 16:09:55 +1000 Subject: [PATCH 040/275] Add missing raise keyword for UDP not supported error --- sshuttle/methods/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index f8a77a941..b1fdeab84 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -72,7 +72,7 @@ def recv_udp(udp_listener, bufsize): def send_udp(self, sock, srcip, dstip, data): if srcip is not None: - Fatal("Method %s send_udp does not support setting srcip to %r" + raise Fatal("Method %s send_udp does not support setting srcip to %r" % (self.name, srcip)) sock.sendto(data, dstip) From 181bf648a7334a42dda0a2b696ced4867ebe08b8 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 16:11:14 +1000 Subject: [PATCH 041/275] Remove useless assignment --- sshuttle/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sshuttle/server.py b/sshuttle/server.py index a9c14228e..cd0c92ae5 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -34,7 +34,6 @@ def _ipmatch(ipstr): elif g[3] is None: ips += '.0' width = min(width, 24) - ips = ips return (struct.unpack('!I', socket.inet_aton(ips))[0], width) From 30cdc5e74b1d5195e0fbe369cf38a008cca2c2d1 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 16:42:47 +1000 Subject: [PATCH 042/275] Fix LGTM reported issues --- setup.py | 2 +- sshuttle/cmdline.py | 3 ++- sshuttle/methods/__init__.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 74c9238e2..22b75a5fe 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def version_scheme(version): "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: " + "License :: OSI Approved :: " + "GNU Lesser General Public License v2 or later (LGPLv2+)", "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index c619cd157..b7ea43f4b 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -33,7 +33,8 @@ def main(): parser.error('exactly zero arguments expected') return firewall.main(opt.method, opt.syslog) elif opt.hostwatch: - return hostwatch.hw_main(opt.subnets, opt.auto_hosts) + hostwatch.hw_main(opt.subnets, opt.auto_hosts) + return 0 else: # parse_subnetports() is used to create a list of includes # and excludes. It is called once for each parameter and diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index b1fdeab84..9aaf62e73 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -7,8 +7,6 @@ def original_dst(sock): - ip = "0.0.0.0" - port = -1 try: family = sock.family SO_ORIGINAL_DST = 80 @@ -73,7 +71,7 @@ def recv_udp(udp_listener, bufsize): def send_udp(self, sock, srcip, dstip, data): if srcip is not None: raise Fatal("Method %s send_udp does not support setting srcip to %r" - % (self.name, srcip)) + % (self.name, srcip)) sock.sendto(data, dstip) def setup_tcp_listener(self, tcp_listener): From 0b267cdeff23e18aca7c1ff758b9f0c65c2f6a1e Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 17:00:57 +1000 Subject: [PATCH 043/275] Create codeql.yml --- .github/workflows/codeql.yml | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..7d89bb851 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '31 21 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From 7d67231faf512d8e6cb36656bc2da8c665b12346 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 17:37:10 +1000 Subject: [PATCH 044/275] Update style issues --- .readthedocs.yaml | 10 +++++----- sshuttle/ssh.py | 2 +- sshuttle/sudoers.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c08429841..217c7df19 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,10 +6,10 @@ build: python: "3.9" sphinx: - configuration: docs/conf.py + configuration: docs/conf.py python: - install: - - requirements: requirements.txt - - method: setuptools - path: . + install: + - requirements: requirements.txt + - method: setuptools + path: . diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 7ced918f1..d7293fa03 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -177,7 +177,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options): # it is present. pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s; exit 97") % \ - (os.devnull, quote(pyscript)) + (os.devnull, quote(pyscript)) pycmd = ("/bin/sh -c {}".format(quote(pycmd))) if password is not None: diff --git a/sshuttle/sudoers.py b/sshuttle/sudoers.py index dece49a18..52874d282 100644 --- a/sshuttle/sudoers.py +++ b/sshuttle/sudoers.py @@ -16,7 +16,7 @@ def build_config(user_name): # sshuttle without needing to enter a sudo password. To use this # configuration, run 'visudo /etc/sudoers.d/sshuttle_auto' as root and # paste this text into the editor that it opens. If you want to give -# multiple users these privilages, you may wish to use use different +# multiple users these privileges, you may wish to use use different # filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john). # This configuration was initially generated by the From 9e3209e93146ceea943d4a993ac6a285babe0f75 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 17:40:43 +1000 Subject: [PATCH 045/275] Remove unused flags assignment --- sshuttle/ssnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index eebe22784..e7ef62314 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -443,7 +443,7 @@ def flush(self): # python < 3.5 flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL) flags |= os.O_NONBLOCK - flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) + fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) if self.outbuf and self.outbuf[0]: wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0]))) @@ -459,7 +459,7 @@ def fill(self): # python < 3.5 flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL) flags |= os.O_NONBLOCK - flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) + fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) try: # If LATENCY_BUFFER_SIZE is inappropriately large, we will # get a MemoryError here. Read no more than 1MiB. From d6fa0c1462ea2fbaa70fde49a20de01035fafd98 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 17:50:05 +1000 Subject: [PATCH 046/275] Replace BaseException with Exception BaseException includes exceptions like SystemExit, KeyboardInterrupt and GeneratorExit that we should not be catching. --- sshuttle/firewall.py | 24 ++++++++++++------------ sshuttle/hostwatch.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 90522c4ab..9108e96cb 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -199,8 +199,8 @@ def main(method_name, syslog): break try: (family, width, exclude, ip, fport, lport) = \ - line.strip().split(',', 5) - except BaseException: + line.strip().split(',', 5) + except Exception: raise Fatal('expected route or NSLIST but got %r' % line) subnets.append(( int(family), @@ -222,7 +222,7 @@ def main(method_name, syslog): break try: (family, ip) = line.strip().split(',', 1) - except BaseException: + except Exception: raise Fatal('expected nslist or PORTS but got %r' % line) nslist.append((int(family), ip)) debug2('Got partial nslist: %r' % nslist) @@ -317,46 +317,46 @@ def main(method_name, syslog): finally: try: debug1('undoing changes.') - except BaseException: + except Exception: debug2('An error occurred, ignoring it.') try: if subnets_v6 or nslist_v6: debug2('undoing IPv6 changes.') method.restore_firewall(port_v6, socket.AF_INET6, udp, user) - except BaseException: + except Exception: try: debug1("Error trying to undo IPv6 firewall.") debug1(traceback.format_exc()) - except BaseException: + except Exception: debug2('An error occurred, ignoring it.') try: if subnets_v4 or nslist_v4: debug2('undoing IPv4 changes.') method.restore_firewall(port_v4, socket.AF_INET, udp, user) - except BaseException: + except Exception: try: debug1("Error trying to undo IPv4 firewall.") debug1(traceback.format_exc()) - except BaseException: + except Exception: debug2('An error occurred, ignoring it.') try: # debug2() message printed in restore_etc_hosts() function. restore_etc_hosts(hostmap, port_v6 or port_v4) - except BaseException: + except Exception: try: debug1("Error trying to undo /etc/hosts changes.") debug1(traceback.format_exc()) - except BaseException: + except Exception: debug2('An error occurred, ignoring it.') try: flush_systemd_dns_cache() - except BaseException: + except Exception: try: debug1("Error trying to flush systemd dns cache.") debug1(traceback.format_exc()) - except BaseException: + except Exception: debug2("An error occurred, ignoring it.") diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py index a016f4f4a..35ab2cc95 100644 --- a/sshuttle/hostwatch.py +++ b/sshuttle/hostwatch.py @@ -55,7 +55,7 @@ def write_host_cache(): try: os.unlink(tmpname) - except BaseException: + except Exception: pass From 004365f5c7a9f59eebffe992c5cbe0eb9e820d59 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 24 Apr 2022 18:38:51 +1000 Subject: [PATCH 047/275] Delete stresstest.py This file has not been touched in years. And looks like it is broken, e.g. listener.accept() is called after infinite loop. --- sshuttle/stresstest.py | 89 ------------------------------------------ 1 file changed, 89 deletions(-) delete mode 100755 sshuttle/stresstest.py diff --git a/sshuttle/stresstest.py b/sshuttle/stresstest.py deleted file mode 100755 index 490e60af8..000000000 --- a/sshuttle/stresstest.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -import socket -import select -import struct -import time - -listener = socket.socket() -listener.bind(('127.0.0.1', 0)) -listener.listen(500) - -servers = [] -clients = [] -remain = {} - -NUMCLIENTS = 50 -count = 0 - - -while 1: - if len(clients) < NUMCLIENTS: - c = socket.socket() - c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - c.bind(('0.0.0.0', 0)) - c.connect(listener.getsockname()) - count += 1 - if count >= 16384: - count = 1 - print('cli CREATING %d' % count) - b = struct.pack('I', count) + 'x' * count - remain[c] = count - print('cli >> %r' % len(b)) - c.send(b) - c.shutdown(socket.SHUT_WR) - clients.append(c) - r = [listener] - time.sleep(0.1) - else: - r = [listener] + servers + clients - print('select(%d)' % len(r)) - r, w, x = select.select(r, [], [], 5) - assert(r) - for i in r: - if i == listener: - s, addr = listener.accept() - servers.append(s) - elif i in servers: - b = i.recv(4096) - print('srv << %r' % len(b)) - if i not in remain: - assert(len(b) >= 4) - want = struct.unpack('I', b[:4])[0] - b = b[4:] - # i.send('y'*want) - else: - want = remain[i] - if want < len(b): - print('weird wanted %d bytes, got %d: %r' % (want, len(b), b)) - assert(want >= len(b)) - want -= len(b) - remain[i] = want - if not b: # EOF - if want: - print('weird: eof but wanted %d more' % want) - assert(want == 0) - i.close() - servers.remove(i) - del remain[i] - else: - print('srv >> %r' % len(b)) - i.send('y' * len(b)) - if not want: - i.shutdown(socket.SHUT_WR) - elif i in clients: - b = i.recv(4096) - print('cli << %r' % len(b)) - want = remain[i] - if want < len(b): - print('weird wanted %d bytes, got %d: %r' % (want, len(b), b)) - assert(want >= len(b)) - want -= len(b) - remain[i] = want - if not b: # EOF - if want: - print('weird: eof but wanted %d more' % want) - assert(want == 0) - i.close() - clients.remove(i) - del remain[i] -listener.accept() From bf4cb64f25b3c290cbbe366e3ed4d02d01edb5b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 10:11:38 +0000 Subject: [PATCH 048/275] Bump pytest from 7.1.1 to 7.1.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.1...7.1.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 664ea4b26..62e2f11fb 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.1.1 +pytest==7.1.2 pytest-cov==3.0.0 flake8==4.0.1 pyflakes==2.4.0 From 553bc2b70ce0ab86a084f229686dcdf03e46e894 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 10:13:29 +0000 Subject: [PATCH 049/275] Bump sphinx from 4.5.0 to 5.0.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.5.0 to 5.0.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.5.0...v5.0.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d5a8119f0..a59523591 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==4.5.0 +Sphinx==5.0.0 From 1def53e08520cf2482bf20445f095dbe5b231c2f Mon Sep 17 00:00:00 2001 From: Nikos Atlas Date: Thu, 19 May 2022 16:41:27 +0300 Subject: [PATCH 050/275] fallback to file editing in case file is locked --- sshuttle/firewall.py | 11 +++++++++-- tests/client/test_firewall.py | 23 +++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 9108e96cb..43e0fb8b9 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -1,4 +1,5 @@ import errno +import shutil import socket import signal import sys @@ -30,7 +31,10 @@ def rewrite_etc_hosts(hostmap, port): else: raise if old_content.strip() and not os.path.exists(BAKFILE): - os.link(HOSTSFILE, BAKFILE) + try: + os.link(HOSTSFILE, BAKFILE) + except OSError: + shutil.copyfile(HOSTSFILE, BAKFILE) tmpname = "%s.%d.tmp" % (HOSTSFILE, port) f = open(tmpname, 'w') for line in old_content.rstrip().split('\n'): @@ -47,7 +51,10 @@ def rewrite_etc_hosts(hostmap, port): else: os.chown(tmpname, 0, 0) os.chmod(tmpname, 0o600) - os.rename(tmpname, HOSTSFILE) + try: + os.rename(tmpname, HOSTSFILE) + except OSError: + shutil.move(tmpname, HOSTSFILE) def restore_etc_hosts(hostmap, port): diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 76aac8935..740432869 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -1,7 +1,11 @@ import io +import os from socket import AF_INET, AF_INET6 from unittest.mock import Mock, patch, call + +import pytest + import sshuttle.firewall @@ -59,6 +63,21 @@ def test_rewrite_etc_hosts(tmpdir): assert orig_hosts.computehash() == new_hosts.computehash() +@patch('os.link') +@patch('os.rename') +def test_rewrite_etc_hosts_no_overwrite(mock_link, mock_rename, tmpdir): + mock_link.side_effect = OSError + mock_rename.side_effect = OSError + + with pytest.raises(OSError): + os.link('/test_from', '/test_to') + + with pytest.raises(OSError): + os.rename('/test_from', '/test_to') + + test_rewrite_etc_hosts(tmpdir) + + def test_subnet_weight(): subnets = [ (AF_INET, 16, 0, '192.168.0.0', 0, 0), @@ -123,7 +142,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), - (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], + (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None, '0x01'), @@ -132,7 +151,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), - (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], + (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None, '0x01'), From 93200f7095fad4a090c075397db91ad80bead976 Mon Sep 17 00:00:00 2001 From: Nikos Atlas Date: Fri, 20 May 2022 12:05:02 +0300 Subject: [PATCH 051/275] add comment and warning --- sshuttle/firewall.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 43e0fb8b9..0e060e2d2 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -34,6 +34,7 @@ def rewrite_etc_hosts(hostmap, port): try: os.link(HOSTSFILE, BAKFILE) except OSError: + # file is locked - performing non-atomic copy shutil.copyfile(HOSTSFILE, BAKFILE) tmpname = "%s.%d.tmp" % (HOSTSFILE, port) f = open(tmpname, 'w') @@ -54,6 +55,9 @@ def rewrite_etc_hosts(hostmap, port): try: os.rename(tmpname, HOSTSFILE) except OSError: + # file is locked - performing non-atomic copy + log('Warning: Using a non-atomic way to overwrite %s that can corrupt the file if ' + 'multiple processes write to it simultaneously.' % HOSTSFILE) shutil.move(tmpname, HOSTSFILE) From 0914bef9a242ec8b16057b8e6c30046190bcf33a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jun 2022 10:27:06 +0000 Subject: [PATCH 052/275] Bump sphinx from 5.0.0 to 5.0.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a59523591..bba2ca616 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==5.0.0 +Sphinx==5.0.1 From 90ec0a9cb671800944fd770288e68daf764a5289 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jun 2022 10:10:55 +0000 Subject: [PATCH 053/275] Bump actions/setup-python from 3 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8b05efbf8..1a450ecf2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From a33a4829e2b1866e281764d13ecfe6da27fbb7a4 Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Tue, 14 Jun 2022 20:02:12 +0200 Subject: [PATCH 054/275] fixed some spelling mistakes --- docs/manpage.rst | 2 +- tests/client/test_methods_nat.py | 2 +- tests/client/test_methods_pf.py | 6 +++--- tests/client/test_methods_tproxy.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 6ea75f3cb..fa1106feb 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -455,7 +455,7 @@ Packet-level forwarding (eg. using the tun/tap devices on Linux) seems elegant at first, but it results in several problems, notably the 'tcp over tcp' problem. The tcp protocol depends fundamentally on packets being dropped -in order to implement its congestion control agorithm; if +in order to implement its congestion control algorithm; if you pass tcp packets through a tcp-based tunnel (such as ssh), the inner tcp packets will never be dropped, and so the inner tcp stream's congestion control will be diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index 6f7ae482b..61b74b5de 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -81,7 +81,7 @@ def test_assert_features(): def test_firewall_command(): method = get_method('nat') - assert not method.firewall_command("somthing") + assert not method.firewall_command("something") @patch('sshuttle.methods.nat.ipt') diff --git a/tests/client/test_methods_pf.py b/tests/client/test_methods_pf.py index dca5c5119..ea7416a0e 100644 --- a/tests/client/test_methods_pf.py +++ b/tests/client/test_methods_pf.py @@ -92,7 +92,7 @@ def test_assert_features(): @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') - assert not method.firewall_command("somthing") + assert not method.firewall_command("something") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, @@ -115,7 +115,7 @@ def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout): @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') - assert not method.firewall_command("somthing") + assert not method.firewall_command("something") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, @@ -138,7 +138,7 @@ def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout): @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') - assert not method.firewall_command("somthing") + assert not method.firewall_command("something") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, diff --git a/tests/client/test_methods_tproxy.py b/tests/client/test_methods_tproxy.py index 994a9075f..d7bc33f5b 100644 --- a/tests/client/test_methods_tproxy.py +++ b/tests/client/test_methods_tproxy.py @@ -78,7 +78,7 @@ def test_assert_features(): def test_firewall_command(): method = get_method('tproxy') - assert not method.firewall_command("somthing") + assert not method.firewall_command("something") @patch('sshuttle.methods.tproxy.ipt') From 7525f8d4c5067b50f0c48f5b5ffbb2a2bdb1e13b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:16:30 +0000 Subject: [PATCH 055/275] Bump sphinx from 5.0.1 to 5.0.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.1...v5.0.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bba2ca616..5308e67ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==6.4.2 -Sphinx==5.0.1 +Sphinx==5.0.2 From 1fa47bf8e17fc80b869c4cbcb66a32e51273fba1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 10:09:19 +0000 Subject: [PATCH 056/275] Bump setuptools-scm from 6.4.2 to 7.0.1 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.2 to 7.0.1. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v6.4.2...v7.0.1) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5308e67ee..b681e0bf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==6.4.2 +setuptools-scm==7.0.1 Sphinx==5.0.2 From f9a9dad9fffb7a7d6c0e38c98259ec663a29e96f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:36:26 +0000 Subject: [PATCH 057/275] Bump setuptools-scm from 7.0.1 to 7.0.2 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.1 to 7.0.2. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v7.0.1...v7.0.2) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b681e0bf8..108996e16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==7.0.1 +setuptools-scm==7.0.2 Sphinx==5.0.2 From df987902069c14392db3a502b398ef4b46cb021f Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 24 Jun 2022 11:02:40 -0400 Subject: [PATCH 058/275] Fix incorrect permissions for /etc/hosts If we modify /etc/hosts, we read/copy the ownership and permissions from the existing /etc/hosts before we make our new temporary file which will eventually overwrite /etc/hosts. If we fail to retrieve the permissions of the existing /etc/hosts file, we made the temporary file owned by root 0o600 permissions. It should have 0o644 permissions so that /etc/hosts has the correct permissions once we rename it. It is unlikely many encoutered this bug since most machines have /etc/hosts prior to sshuttle running and we should be able to read the permission/ownership of that existing file. --- sshuttle/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 0e060e2d2..b184d9bb2 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -51,7 +51,7 @@ def rewrite_etc_hosts(hostmap, port): os.chmod(tmpname, st.st_mode) else: os.chown(tmpname, 0, 0) - os.chmod(tmpname, 0o600) + os.chmod(tmpname, 0o644) try: os.rename(tmpname, HOSTSFILE) except OSError: From 86c69dda48393dc44e690e33c5c6250a521bf0a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 10:13:28 +0000 Subject: [PATCH 059/275] Bump setuptools-scm from 7.0.2 to 7.0.3 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 108996e16..94d16b640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==7.0.2 +setuptools-scm==7.0.3 Sphinx==5.0.2 From 2462d6d2049561187925d9f7aea1c4d607188678 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:12:51 +0000 Subject: [PATCH 060/275] Bump setuptools-scm from 7.0.3 to 7.0.4 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94d16b640..80ff66c06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==7.0.3 +setuptools-scm==7.0.4 Sphinx==5.0.2 From bf4fa6cacc2e24f3f8313ed3e8c33a6100cdab25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Jul 2022 10:11:07 +0000 Subject: [PATCH 061/275] Bump setuptools-scm from 7.0.4 to 7.0.5 Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.4 to 7.0.5. - [Release notes](https://github.com/pypa/setuptools_scm/releases) - [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/setuptools_scm/compare/v7.0.4...v7.0.5) --- updated-dependencies: - dependency-name: setuptools-scm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80ff66c06..7fea1fb23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -setuptools-scm==7.0.4 +setuptools-scm==7.0.5 Sphinx==5.0.2 From 6929b79274a7b623d7b894f6ece1d7dab864f2f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 10:21:55 +0000 Subject: [PATCH 062/275] Bump sphinx from 5.0.2 to 5.1.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.2 to 5.1.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.2...v5.1.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fea1fb23..c572123f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==7.0.5 -Sphinx==5.0.2 +Sphinx==5.1.0 From 58d72a93d265955c609396f79142a491a994c7c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:11:39 +0000 Subject: [PATCH 063/275] Bump sphinx from 5.1.0 to 5.1.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.0 to 5.1.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.0...v5.1.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c572123f6..3965bfc3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ setuptools-scm==7.0.5 -Sphinx==5.1.0 +Sphinx==5.1.1 From f8086dfa590f742561ffc1f4b4363da3f3cf4069 Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 5 Aug 2022 08:00:56 +1000 Subject: [PATCH 064/275] Update flake8 and pyflakes --- requirements-tests.txt | 4 ++-- sshuttle/client.py | 18 +++++++++--------- sshuttle/firewall.py | 16 ++++++++-------- sshuttle/methods/ipfw.py | 4 ++-- sshuttle/methods/tproxy.py | 2 +- sshuttle/server.py | 4 ++-- sshuttle/ssnet.py | 12 ++++++------ 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 62e2f11fb..0c310045d 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==7.1.2 pytest-cov==3.0.0 -flake8==4.0.1 -pyflakes==2.4.0 +flake8==5.0.4 +pyflakes==2.5.0 diff --git a/sshuttle/client.py b/sshuttle/client.py index 51af6a0a1..49bb7e2e7 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -123,14 +123,14 @@ def __init__(self, kind=socket.SOCK_STREAM, proto=0): self.bind_called = False def setsockopt(self, level, optname, value): - assert(self.bind_called) + assert self.bind_called if self.v6: self.v6.setsockopt(level, optname, value) if self.v4: self.v4.setsockopt(level, optname, value) def add_handler(self, handlers, callback, method, mux): - assert(self.bind_called) + assert self.bind_called socks = [] if self.v6: socks.append(self.v6) @@ -145,7 +145,7 @@ def add_handler(self, handlers, callback, method, mux): ) def listen(self, backlog): - assert(self.bind_called) + assert self.bind_called if self.v6: self.v6.listen(backlog) if self.v4: @@ -160,7 +160,7 @@ def listen(self, backlog): raise e def bind(self, address_v6, address_v4): - assert(not self.bind_called) + assert not self.bind_called self.bind_called = True if address_v6 is not None: self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto) @@ -189,7 +189,7 @@ def bind(self, address_v6, address_v4): self.v4 = None def print_listening(self, what): - assert(self.bind_called) + assert self.bind_called if self.v6: listenip = self.v6.getsockname() debug1('%s listening on %r.' % (what, listenip)) @@ -374,8 +374,8 @@ def start(self): raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) def sethostip(self, hostname, ip): - assert(not re.search(br'[^-\w\.]', hostname)) - assert(not re.search(br'[^0-9.]', ip)) + assert not re.search(br'[^-\w\.]', hostname) + assert not re.search(br'[^0-9.]', ip) self.pfile.write(b'HOST %s,%s\n' % (hostname, ip)) self.pfile.flush() @@ -973,7 +973,7 @@ def feature_status(label, enabled, available): raise e if not bound: - assert(last_e) + assert last_e raise last_e tcp_listener.listen(10) tcp_listener.print_listening("TCP redirector") @@ -1019,7 +1019,7 @@ def feature_status(label, enabled, available): dns_listener.print_listening("DNS") if not bound: - assert(last_e) + assert last_e raise last_e else: dnsport_v6 = 0 diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index b184d9bb2..af71fe7b2 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -250,14 +250,14 @@ def main(method_name, syslog): dnsport_v6 = int(ports[2]) dnsport_v4 = int(ports[3]) - assert(port_v6 >= 0) - assert(port_v6 <= 65535) - assert(port_v4 >= 0) - assert(port_v4 <= 65535) - assert(dnsport_v6 >= 0) - assert(dnsport_v6 <= 65535) - assert(dnsport_v4 >= 0) - assert(dnsport_v4 <= 65535) + assert port_v6 >= 0 + assert port_v6 <= 65535 + assert port_v4 >= 0 + assert port_v4 <= 65535 + assert dnsport_v6 >= 0 + assert dnsport_v6 <= 65535 + assert dnsport_v4 >= 0 + assert dnsport_v4 <= 65535 debug2('Got ports: %d,%d,%d,%d' % (port_v6, port_v4, dnsport_v6, dnsport_v4)) diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py index 1a31e025e..74fd9f747 100644 --- a/sshuttle/methods/ipfw.py +++ b/sshuttle/methods/ipfw.py @@ -52,7 +52,7 @@ def _fill_oldctls(prefix): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env()) for line in p.stdout: line = line.decode() - assert(line[-1] == '\n') + assert line[-1] == '\n' (k, v) = line[:-1].split(': ', 1) _oldctls[k] = v.strip() rv = p.wait() @@ -74,7 +74,7 @@ def _sysctl_set(name, val): def sysctl_set(name, val, permanent=False): PREFIX = 'net.inet.ip' - assert(name.startswith(PREFIX + '.')) + assert name.startswith(PREFIX + '.') val = str(val) if not _oldctls: _fill_oldctls(PREFIX) diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 3450433aa..1d2ae29b9 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -127,7 +127,7 @@ def _ipt(*args): def _ipt_proto_ports(proto, fport, lport): return proto + ('--dport', '%d:%d' % (fport, lport)) \ - if fport else proto + if fport else proto mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port diff --git a/sshuttle/server.py b/sshuttle/server.py index cd0c92ae5..5aff9086e 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -302,7 +302,7 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver, hw.leftover = b('') def hostwatch_ready(sock): - assert(hw.pid) + assert hw.pid content = hw.sock.recv(4096) if content: lines = (hw.leftover + content).split(b('\n')) @@ -380,7 +380,7 @@ def udp_open(channel, data): while mux.ok: if hw.pid: - assert(hw.pid > 0) + assert hw.pid > 0 (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) if rpid: raise Fatal( diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index e7ef62314..c1c5d76af 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -227,7 +227,7 @@ def uwrite(self, buf): return 0 def write(self, buf): - assert(buf) + assert buf return self.uwrite(buf) def uread(self): @@ -402,15 +402,15 @@ def got_packet(self, channel, cmd, data): elif cmd == CMD_EXIT: self.ok = False elif cmd == CMD_TCP_CONNECT: - assert(not self.channels.get(channel)) + assert not self.channels.get(channel) if self.new_channel: self.new_channel(channel, data) elif cmd == CMD_DNS_REQ: - assert(not self.channels.get(channel)) + assert not self.channels.get(channel) if self.got_dns_req: self.got_dns_req(channel, data) elif cmd == CMD_UDP_OPEN: - assert(not self.channels.get(channel)) + assert not self.channels.get(channel) if self.got_udp_open: self.got_udp_open(channel, data) elif cmd == CMD_ROUTES: @@ -482,8 +482,8 @@ def handle(self): if len(self.inbuf) >= (self.want or HDR_LEN): (s1, s2, channel, cmd, datalen) = \ struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) - assert(s1 == b('S')) - assert(s2 == b('S')) + assert s1 == b('S') + assert s2 == b('S') self.want = datalen + HDR_LEN if self.want and len(self.inbuf) >= self.want: data = self.inbuf[HDR_LEN:self.want] From a6efc6b653d01ba7d7d34fa9dde2fd953c4c926a Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Mon, 29 Aug 2022 10:28:45 +0200 Subject: [PATCH 065/275] This test broke in Python3.11 Fixed the test and only check for instance: str for families that are not explicitly covered in `family_to_str` closes: #784 --- tests/client/test_helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py index 8e69d45d1..1e8d6bb91 100644 --- a/tests/client/test_helpers.py +++ b/tests/client/test_helpers.py @@ -192,5 +192,4 @@ def test_family_ip_tuple(): def test_family_to_string(): assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET" assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6" - expected = 'AddressFamily.AF_UNIX' - assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected + assert isinstance(sshuttle.helpers.family_to_string(socket.AF_UNIX), str) From 77eb8167c46468bcb00bc97f07cb0187ae7627ae Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Mon, 29 Aug 2022 10:32:28 +0200 Subject: [PATCH 066/275] all elements are strings --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1a450ecf2..de573f810 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 From 98233530a0e46a56c1811436c7ce68ba7973541c Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Mon, 29 Aug 2022 11:00:16 +0200 Subject: [PATCH 067/275] remove setuptools-scm --- requirements.txt | 1 - setup.py | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3965bfc3d..baa05d40d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -setuptools-scm==7.0.5 Sphinx==5.1.1 diff --git a/setup.py b/setup.py index 22b75a5fe..00f683c66 100755 --- a/setup.py +++ b/setup.py @@ -20,19 +20,8 @@ from setuptools import setup, find_packages -def version_scheme(version): - from setuptools_scm.version import guess_next_dev_version - version = guess_next_dev_version(version) - return version.lstrip("v") - - setup( name="sshuttle", - use_scm_version={ - 'write_to': "sshuttle/version.py", - 'version_scheme': version_scheme, - }, - setup_requires=['setuptools_scm'], # version=version, url='https://github.com/sshuttle/sshuttle', author='Brian May', From 810b4a31703f71439f1e1a22e59e05e12b66ef39 Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Mon, 29 Aug 2022 11:30:59 +0200 Subject: [PATCH 068/275] added bump2version --- .gitignore | 1 - requirements-tests.txt | 1 + setup.cfg | 19 ++++++++++++------- sshuttle/version.py | 1 + 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 sshuttle/version.py diff --git a/.gitignore b/.gitignore index b79343aa4..1f755b631 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/sshuttle/version.py /tmp/ /.cache/ /.eggs/ diff --git a/requirements-tests.txt b/requirements-tests.txt index 0c310045d..7aa29682e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,3 +3,4 @@ pytest==7.1.2 pytest-cov==3.0.0 flake8==5.0.4 pyflakes==2.5.0 +bump2version==1.0.1 diff --git a/setup.cfg b/setup.cfg index dbdf1616a..f4abe5564 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,18 +1,23 @@ +[bumpversion] +current_version = 1.1.0 + +[bumpversion:file:sshuttle/version.py] + [aliases] -test=pytest +test = pytest [bdist_wheel] universal = 1 [upload] -sign=true -identity=0x1784577F811F6EAC +sign = true +identity = 0x1784577F811F6EAC [flake8] -count=true -show-source=true -statistics=true -max-line-length=128 +count = true +show-source = true +statistics = true +max-line-length = 128 [tool:pytest] addopts = --cov=sshuttle --cov-branch --cov-report=term-missing diff --git a/sshuttle/version.py b/sshuttle/version.py new file mode 100644 index 000000000..d4f3c4c73 --- /dev/null +++ b/sshuttle/version.py @@ -0,0 +1 @@ +__version__ = version = '1.1.0' From 88139ed2e59ac0d261420754aa0ef95803133648 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 15:19:46 +0000 Subject: [PATCH 069/275] Bump pytest from 7.1.2 to 7.1.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.2...7.1.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 7aa29682e..d0e518104 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.1.2 +pytest==7.1.3 pytest-cov==3.0.0 flake8==5.0.4 pyflakes==2.5.0 From a1c7e64b0e0e9fb668babc0a79ccf5397e4fbb6d Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 6 Sep 2022 08:04:28 +1000 Subject: [PATCH 070/275] Add .coverage to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1f755b631..022142177 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /tmp/ +/.coverage /.cache/ /.eggs/ /.tox/ From 4f4d6d9f4d83bdbe5f4123b52eb4fadcfba6c974 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 6 Sep 2022 08:06:34 +1000 Subject: [PATCH 071/275] Add ASDF .tool-versions file --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..cb51a3125 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.10.6 From a28c8ae10b8d4be847f1414119408f3e076aa8fe Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 6 Sep 2022 08:16:24 +1000 Subject: [PATCH 072/275] Include version in setup.py too --- setup.cfg | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f4abe5564..f4be2cdf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,8 @@ [bumpversion] current_version = 1.1.0 +[bumpversion:file:setup.py] + [bumpversion:file:sshuttle/version.py] [aliases] diff --git a/setup.py b/setup.py index 00f683c66..26c6a7c9f 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="sshuttle", - # version=version, + version='1.1.0', url='https://github.com/sshuttle/sshuttle', author='Brian May', author_email='brian@linuxpenguins.xyz', From 9df7a0a053042c1b58d3f84fe088d77ca1e29b21 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 6 Sep 2022 08:17:47 +1000 Subject: [PATCH 073/275] =?UTF-8?q?Bump=20version:=201.1.0=20=E2=86=92=201?= =?UTF-8?q?.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- sshuttle/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index f4be2cdf5..59a9b1231 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.0 +current_version = 1.1.1 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 26c6a7c9f..4d4925764 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="sshuttle", - version='1.1.0', + version='1.1.1', url='https://github.com/sshuttle/sshuttle', author='Brian May', author_email='brian@linuxpenguins.xyz', diff --git a/sshuttle/version.py b/sshuttle/version.py index d4f3c4c73..b5b4f934c 100644 --- a/sshuttle/version.py +++ b/sshuttle/version.py @@ -1 +1 @@ -__version__ = version = '1.1.0' +__version__ = version = '1.1.1' From 060f849c7e2ab3a233149681ec03ef6d98211ce8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 10:26:53 +0000 Subject: [PATCH 074/275] Bump sphinx from 5.1.1 to 5.2.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.1 to 5.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.1...v5.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index baa05d40d..03f3ac22c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==5.1.1 +Sphinx==5.2.1 From 1d240e0cd9d1bfafc204bacee8f85ab776bd63f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 10:09:10 +0000 Subject: [PATCH 075/275] Bump sphinx from 5.2.1 to 5.2.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.1 to 5.2.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.1...v5.2.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 03f3ac22c..b9af2fd15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==5.2.1 +Sphinx==5.2.2 From d99940c58e360138dea22e613e27f3205d5adc2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Sep 2022 10:19:09 +0000 Subject: [PATCH 076/275] Bump pytest-cov from 3.0.0 to 4.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.0.0. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index d0e518104..6acaaccd9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,6 @@ -r requirements.txt pytest==7.1.3 -pytest-cov==3.0.0 +pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 bump2version==1.0.1 From e704ea74e515e36dde645a2bbfb60a0bf9fe9580 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 10:09:37 +0000 Subject: [PATCH 077/275] Bump sphinx from 5.2.2 to 5.2.3 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.2 to 5.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.2...v5.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b9af2fd15..b7525306b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==5.2.2 +Sphinx==5.2.3 From ad05994e651c2c7db394efe6a5c32746c9872944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 10:11:43 +0000 Subject: [PATCH 078/275] Bump sphinx from 5.2.3 to 5.3.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.3 to 5.3.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.3...v5.3.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b7525306b..dd8706637 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==5.2.3 +Sphinx==5.3.0 From 53da0368792198bb94cb3e031a9b7142cf0bcea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:10:24 +0000 Subject: [PATCH 079/275] Bump pytest from 7.1.3 to 7.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.3...7.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6acaaccd9..a2694911b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.1.3 +pytest==7.2.0 pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 From b8e6ebf74152a725c35fc5a447ab61cec2a129c1 Mon Sep 17 00:00:00 2001 From: dinosaurtirex <58666088+dinosaurtirex@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:17:34 +0300 Subject: [PATCH 080/275] Removed a little bit of legacy code Removed a few lines of legacy code (to make it look more clean) --- sshuttle/ssnet.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index c1c5d76af..19d60ef61 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -193,7 +193,6 @@ def noread(self): if not self.shut_read: debug2('%r: done reading' % self) self.shut_read = True - # self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway def nowrite(self): if not self.shut_write: @@ -373,11 +372,6 @@ def check_fullness(self): if not self.too_full: self.send(0, CMD_PING, b('rttest')) self.too_full = True - # ob = [] - # for b in self.outbuf: - # (s1,s2,c) = struct.unpack('!ccH', b[:4]) - # ob.append(c) - # log('outbuf: %d %r' % (self.amount_queued(), ob)) def send(self, channel, cmd, data): assert isinstance(data, bytes) @@ -476,8 +470,6 @@ def fill(self): def handle(self): self.fill() - # log('inbuf is: (%d,%d) %r' - # % (self.want, len(self.inbuf), self.inbuf)) while 1: if len(self.inbuf) >= (self.want or HDR_LEN): (s1, s2, channel, cmd, datalen) = \ From f23b24b74edf1fef9981a1f2e84864a6fda4aefd Mon Sep 17 00:00:00 2001 From: rmchale Date: Mon, 12 Dec 2022 14:30:39 -0600 Subject: [PATCH 081/275] Update ssyslog.py --- sshuttle/ssyslog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/ssyslog.py b/sshuttle/ssyslog.py index 630c00e94..30118723b 100644 --- a/sshuttle/ssyslog.py +++ b/sshuttle/ssyslog.py @@ -10,7 +10,7 @@ def start_syslog(): global _p with open(os.devnull, 'w') as devnull: _p = ssubprocess.Popen( - ['logger', '-p', 'daemon.notice', '-t', 'sshuttle'], + ['logger', '-p', 'daemon.err', '-t', 'sshuttle'], stdin=ssubprocess.PIPE, stdout=devnull, stderr=devnull From 57111d7a1302c823e3d6372d633c0c1081d95fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Dec 2022 10:02:05 +0000 Subject: [PATCH 082/275] Bump sphinx from 5.3.0 to 6.0.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.3.0 to 6.0.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.3.0...v6.0.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dd8706637..7a2e01245 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==5.3.0 +Sphinx==6.0.0 From 8ba8dff71947c1c65c9b1246a1cf0190f9cc4242 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 10:02:26 +0000 Subject: [PATCH 083/275] Bump sphinx from 6.0.0 to 6.1.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.0.0 to 6.1.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/v6.1.1/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.0.0...v6.1.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a2e01245..f18b82961 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.0.0 +Sphinx==6.1.1 From 7edc7ba7bc1a5bb4d8813a719d71a27d797eda36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:04:31 +0000 Subject: [PATCH 084/275] Bump sphinx from 6.1.1 to 6.1.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.1...v6.1.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f18b82961..8f65fe1fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.1.1 +Sphinx==6.1.2 From 23207f27faa39920bf63e91d67d4d31d81fe0161 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:02:43 +0000 Subject: [PATCH 085/275] Bump sphinx from 6.1.2 to 6.1.3 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.2 to 6.1.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.2...v6.1.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f65fe1fd..21b1000d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.1.2 +Sphinx==6.1.3 From faf34e14e0904ffecbd3082d7fd41689ea7d1f6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 10:03:41 +0000 Subject: [PATCH 086/275] Bump pytest from 7.2.0 to 7.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.0 to 7.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.0...7.2.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index a2694911b..125d345a3 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.2.0 +pytest==7.2.1 pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 From e0ef2964cd9e9f372a819d41b979786c167f53d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:58:33 +0000 Subject: [PATCH 087/275] Bump pytest from 7.2.1 to 7.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 125d345a3..07b9fda59 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.2.1 +pytest==7.2.2 pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 From fd8a0b624d8f5d389136dbda32466ee4a573951b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 10:57:28 +0000 Subject: [PATCH 088/275] Bump pytest from 7.2.2 to 7.3.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.2...7.3.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 07b9fda59..33537a952 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.2.2 +pytest==7.3.0 pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 From d336002833f41b8267c12b3120cfb9ac4f30a73a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 10:58:00 +0000 Subject: [PATCH 089/275] Bump pytest from 7.3.0 to 7.3.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.0 to 7.3.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.0...7.3.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 33537a952..1e21786eb 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.3.0 +pytest==7.3.1 pytest-cov==4.0.0 flake8==5.0.4 pyflakes==2.5.0 From a54fd8ab4e886ca7fbf843901d367163614dd34d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:57:57 +0000 Subject: [PATCH 090/275] Bump sphinx from 6.1.3 to 6.2.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.3 to 6.2.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.3...v6.2.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 21b1000d3..9c78ddff3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.1.3 +Sphinx==6.2.0 From 25cd95130dd9cc897ed8de6e7e7e8d44d0aaf028 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:07:30 +0000 Subject: [PATCH 091/275] Bump sphinx from 6.2.0 to 6.2.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.0 to 6.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.0...v6.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9c78ddff3..5d376f521 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.2.0 +Sphinx==6.2.1 From dffc1c7f929d2a41ef84929318537ef41352ba01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 10:58:58 +0000 Subject: [PATCH 092/275] Bump sphinx from 6.2.1 to 7.0.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.0.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.0.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5d376f521..526215aee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.2.1 +Sphinx==7.0.0 From 5778437148b94aaccf6ee5011dfb00165777bfc3 Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 3 May 2023 09:48:20 +1000 Subject: [PATCH 093/275] Revert "Bump sphinx from 6.2.1 to 7.0.0" This reverts commit dffc1c7f929d2a41ef84929318537ef41352ba01. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 526215aee..5d376f521 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==7.0.0 +Sphinx==6.2.1 From 799c9f33d04f56e39a80240276e5508488f82132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 10:57:38 +0000 Subject: [PATCH 094/275] Bump pytest-cov from 4.0.0 to 4.1.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.0.0 to 4.1.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 1e21786eb..d3ec42c34 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,6 @@ -r requirements.txt pytest==7.3.1 -pytest-cov==4.0.0 +pytest-cov==4.1.0 flake8==5.0.4 pyflakes==2.5.0 bump2version==1.0.1 From a289580f24e7604b957b39e1df169894dd8d5400 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:57:48 +0000 Subject: [PATCH 095/275] Bump pytest from 7.3.1 to 7.3.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.1...7.3.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index d3ec42c34..4a9ae7f9b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.3.1 +pytest==7.3.2 pytest-cov==4.1.0 flake8==5.0.4 pyflakes==2.5.0 From 4e592265f67966a431571d7281cf27096b7b6646 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 10:57:30 +0000 Subject: [PATCH 096/275] Bump pytest from 7.3.2 to 7.4.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.2 to 7.4.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.2...7.4.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 4a9ae7f9b..e2344b4ce 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.3.2 +pytest==7.4.0 pytest-cov==4.1.0 flake8==5.0.4 pyflakes==2.5.0 From 17bfdc24b8f046962f45757917aaa0ce20ddcd7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 10:48:17 +0000 Subject: [PATCH 097/275] Bump sphinx from 6.2.1 to 7.1.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.1.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.1.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5d376f521..7f9c5684c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Sphinx==6.2.1 +Sphinx==7.1.1 From 802c6f5a6e0b80c1a8b038f809d6077d8f8d956a Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 31 Jul 2023 08:17:33 +1000 Subject: [PATCH 098/275] Use furo style for docs The default read the docs theme does not work with the latest Sphinx. --- docs/conf.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 6b15f80f1..c69f00197 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,7 +103,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/requirements.txt b/requirements.txt index 7f9c5684c..1122fc54e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Sphinx==7.1.1 +furo==2023.5.20 From f597e70ae6f60647c4e060108a7952a153f66b52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:38:22 +0000 Subject: [PATCH 099/275] Bump furo from 2023.5.20 to 2023.7.26 Bumps [furo](https://github.com/pradyunsg/furo) from 2023.5.20 to 2023.7.26. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.05.20...2023.07.26) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1122fc54e..c9141403d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.1 -furo==2023.5.20 +furo==2023.7.26 From ac06e7968fde6773d9dfb0a8d79a0cf94e8014ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 10:23:23 +0000 Subject: [PATCH 100/275] Bump sphinx from 7.1.1 to 7.1.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.1...v7.1.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9141403d..c6e9afcfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Sphinx==7.1.1 +Sphinx==7.1.2 furo==2023.7.26 From 6b7cf80420e16ca58fbb6e28ab4adb030436636a Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Fri, 4 Aug 2023 18:49:58 +0700 Subject: [PATCH 101/275] Add support for group-based routing --- sshuttle/client.py | 17 +++++++++++++++-- sshuttle/cmdline.py | 1 + sshuttle/methods/__init__.py | 1 + sshuttle/methods/ipfw.py | 4 ++-- sshuttle/methods/nat.py | 28 ++++++++++++++++++++-------- sshuttle/methods/nft.py | 4 ++-- sshuttle/methods/pf.py | 4 ++-- sshuttle/methods/tproxy.py | 4 ++-- sshuttle/options.py | 6 ++++++ 9 files changed, 51 insertions(+), 18 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 49bb7e2e7..25b344050 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -21,6 +21,10 @@ from pwd import getpwnam except ImportError: getpwnam = None +try: + from grp import getgrnam +except ImportError: + getgrnam = None import socket @@ -726,7 +730,7 @@ def main(listenip_v6, listenip_v4, latency_buffer_size, dns, nslist, method_name, seed_hosts, auto_hosts, auto_nets, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, - user, sudo_pythonpath, tmark): + user, group, sudo_pythonpath, tmark): if not remotename: raise Fatal("You must use -r/--remote to specify a remote " @@ -829,6 +833,15 @@ def main(listenip_v6, listenip_v4, raise Fatal("User %s does not exist." % user) required.user = False if user is None else True + if group is not None: + if getgrnam is None: + raise Fatal("Routing by group not available on this system.") + try: + group = getgrnam(group).gr_gid + except KeyError: + raise Fatal("User %s does not exist." % user) + required.group = False if group is None else True + if not required.ipv6 and len(subnets_v6) > 0: print("WARNING: IPv6 subnets were ignored because IPv6 is disabled " "in sshuttle.") @@ -1058,7 +1071,7 @@ def feature_status(label, enabled, available): # start the firewall fw.setup(subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, - required.udp, user, tmark) + required.udp, user, group, tmark) # start the client process try: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index b7ea43f4b..eaca961d8 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -104,6 +104,7 @@ def main(): opt.to_ns, opt.pidfile, opt.user, + opt.group, opt.sudo_pythonpath, opt.tmark) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 9aaf62e73..4a1abe68d 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -50,6 +50,7 @@ def get_supported_features(): result.udp = False result.dns = True result.user = False + result.group = False return result @staticmethod diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py index 74fd9f747..053ddf37b 100644 --- a/sshuttle/methods/ipfw.py +++ b/sshuttle/methods/ipfw.py @@ -156,7 +156,7 @@ def setup_udp_listener(self, udp_listener): # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): # IPv6 not supported if family not in [socket.AF_INET]: raise Exception( @@ -207,7 +207,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, else: ipfw('table', '126', 'add', '%s/%s' % (snet, swidth)) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET]: raise Exception( 'Address family "%s" unsupported by ipfw method' diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 076d880d3..de950080f 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family != socket.AF_INET and family != socket.AF_INET6: raise Exception( 'Address family "%s" unsupported by nat method_name' @@ -35,9 +35,14 @@ def _ipm(*args): _ipt('-N', chain) _ipt('-F', chain) - if user is not None: - _ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user), - '-j', 'MARK', '--set-mark', str(port)) + if user is not None or group is not None: + margs = ['-I', 'OUTPUT', '1', '-m', 'owner'] + if user is not None: + margs.append('--uid-owner', str(user)) + if group is not None: + margs.append('--gid-owner', str(group)) + margs = args.append('-j', 'MARK', '--set-mark', str(port)) + nonfatal(_ipm, *margs) args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain @@ -75,7 +80,7 @@ def _ipm(*args): '--dest', '%s/%s' % (snet, swidth), *(tcp_ports + ('--to-ports', str(port)))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): # only ipv4 supported with NAT if family != socket.AF_INET and family != socket.AF_INET6: raise Exception( @@ -96,9 +101,15 @@ def _ipm(*args): # basic cleanup/setup of chains if ipt_chain_exists(family, table, chain): - if user is not None: - nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner', - str(user), '-j', 'MARK', '--set-mark', str(port)) + if user is not None or group is not None: + margs = ['-D', 'OUTPUT', '-m', 'owner'] + if user is not None: + margs.append('--uid-owner', str(user)) + if group is not None: + margs.append('--gid-owner', str(group)) + margs = args.append('-j', 'MARK', '--set-mark', str(port)) + nonfatal(_ipm, *margs) + args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain @@ -111,6 +122,7 @@ def get_supported_features(self): result = super(Method, self).get_supported_features() result.user = True result.ipv6 = True + result.group = True return result def is_supported(self): diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py index 64ab3a6d8..59b6310d8 100644 --- a/sshuttle/methods/nft.py +++ b/sshuttle/methods/nft.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if udp: raise Exception("UDP not supported by nft") @@ -87,7 +87,7 @@ def _nft(action, *args): ip_version, 'daddr %s/%s' % (snet, swidth), ('redirect to :' + str(port))))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if udp: raise Exception("UDP not supported by nft method_name") diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index ed56c514a..d5ed06af5 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -448,7 +448,7 @@ def get_tcp_dstip(self, sock): return sock.getsockname() def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' @@ -473,7 +473,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, pf.add_rules(anchor, includes, port, dnsport, nslist, family) pf.enable() - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 1d2ae29b9..e12943c9b 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -114,7 +114,7 @@ def setup_udp_listener(self, udp_listener): udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by tproxy method' @@ -228,7 +228,7 @@ def _ipt_proto_ports(proto, fport, lport): '-m', 'udp', *(udp_ports + ('--on-port', str(port)))) - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by tproxy method' diff --git a/sshuttle/options.py b/sshuttle/options.py index 0ac7690db..acd46da58 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -382,6 +382,12 @@ def convert_arg_line_to_args(self, arg_line): apply all the rules only to this linux user """ ) +parser.add_argument( + "--group", + help=""" + apply all the rules only to this linux group + """ +) parser.add_argument( "--firewall", action="http://github.com/store_true", From 755e522effe2f9eb9262239fd047812a41ce25bf Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Fri, 4 Aug 2023 19:09:14 +0700 Subject: [PATCH 102/275] Allow user to tunnel traffic to local port --- sshuttle/methods/nat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index de950080f..1254e55fb 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -59,11 +59,6 @@ def _ipm(*args): '--dport', '53', '--to-ports', str(dnsport)) - # Don't route any remaining local traffic through sshuttle. - _ipt('-A', chain, '-j', 'RETURN', - '-m', 'addrtype', - '--dst-type', 'LOCAL') - # create new subnet entries. for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): @@ -79,6 +74,11 @@ def _ipm(*args): _ipt('-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet, swidth), *(tcp_ports + ('--to-ports', str(port)))) + + # Don't route any remaining local traffic through sshuttle. + _ipt('-A', chain, '-j', 'RETURN', + '-m', 'addrtype', + '--dst-type', 'LOCAL') def restore_firewall(self, port, family, udp, user, group): # only ipv4 supported with NAT From 7c140daf0731887874610bc454809539828308e6 Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Fri, 4 Aug 2023 19:50:57 +0700 Subject: [PATCH 103/275] Pass group to firewall --- sshuttle/client.py | 14 ++++++++++---- sshuttle/firewall.py | 16 +++++++++------- sshuttle/methods/__init__.py | 4 ++-- sshuttle/methods/nat.py | 14 +++++++------- sshuttle/methods/tproxy.py | 2 +- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 25b344050..5d1d990de 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -319,7 +319,7 @@ def setup(): def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, - user, tmark): + user, group, tmark): self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.nslist = nslist @@ -329,6 +329,7 @@ def setup(self, subnets_include, subnets_exclude, nslist, self.dnsport_v4 = dnsport_v4 self.udp = udp self.user = user + self.group = group self.tmark = tmark def check(self): @@ -367,9 +368,14 @@ def start(self): user = bytes(self.user, 'utf-8') else: user = b'%d' % self.user - - self.pfile.write(b'GO %d %s %s %d\n' % - (udp, user, bytes(self.tmark, 'ascii'), os.getpid())) + if self.group is None: + group = b'-' + elif isinstance(self.group, str): + group = bytes(self.group, 'utf-8') + else: + group = b'%d' % self.group + self.pfile.write(b'GO %d %s %s %s %d\n' % + (udp, user, group, bytes(self.tmark, 'ascii'), os.getpid())) self.pfile.flush() line = self.pfile.readline() diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index af71fe7b2..60662b9dd 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -270,13 +270,15 @@ def main(method_name, syslog): _, _, args = line.partition(" ") global sshuttle_pid - udp, user, tmark, sshuttle_pid = args.strip().split(" ", 3) + udp, user, group, tmark, sshuttle_pid = args.strip().split(" ", 4) udp = bool(int(udp)) sshuttle_pid = int(sshuttle_pid) if user == '-': user = None - debug2('Got udp: %r, user: %r, tmark: %s, sshuttle_pid: %d' % - (udp, user, tmark, sshuttle_pid)) + if group == '-': + group = None + debug2('Got udp: %r, user: %r, group: %r, tmark: %s, sshuttle_pid: %d' % + (udp, user, group, tmark, sshuttle_pid)) subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] @@ -291,14 +293,14 @@ def main(method_name, syslog): method.setup_firewall( port_v6, dnsport_v6, nslist_v6, socket.AF_INET6, subnets_v6, udp, - user, tmark) + user, group, tmark) if subnets_v4 or nslist_v4: debug2('setting up IPv4.') method.setup_firewall( port_v4, dnsport_v4, nslist_v4, socket.AF_INET, subnets_v4, udp, - user, tmark) + user, group, tmark) flush_systemd_dns_cache() stdout.write('STARTED\n') @@ -334,7 +336,7 @@ def main(method_name, syslog): try: if subnets_v6 or nslist_v6: debug2('undoing IPv6 changes.') - method.restore_firewall(port_v6, socket.AF_INET6, udp, user) + method.restore_firewall(port_v6, socket.AF_INET6, udp, user, group) except Exception: try: debug1("Error trying to undo IPv6 firewall.") @@ -345,7 +347,7 @@ def main(method_name, syslog): try: if subnets_v4 or nslist_v4: debug2('undoing IPv4 changes.') - method.restore_firewall(port_v4, socket.AF_INET, udp, user) + method.restore_firewall(port_v4, socket.AF_INET, udp, user, group) except Exception: try: debug1("Error trying to undo IPv4 firewall.") diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 4a1abe68d..962529b6c 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -90,10 +90,10 @@ def assert_features(self, features): (key, self.name)) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): + user, group, tmark): raise NotImplementedError() - def restore_firewall(self, port, family, udp, user): + def restore_firewall(self, port, family, udp, user, group): raise NotImplementedError() @staticmethod diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 1254e55fb..bd878b11c 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -31,17 +31,17 @@ def _ipm(*args): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - self.restore_firewall(port, family, udp, user) + self.restore_firewall(port, family, udp, user, group) _ipt('-N', chain) _ipt('-F', chain) if user is not None or group is not None: margs = ['-I', 'OUTPUT', '1', '-m', 'owner'] if user is not None: - margs.append('--uid-owner', str(user)) + margs += ['--uid-owner', str(user)] if group is not None: - margs.append('--gid-owner', str(group)) - margs = args.append('-j', 'MARK', '--set-mark', str(port)) + margs += ['--gid-owner', str(group)] + margs += ['-j', 'MARK', '--set-mark', str(port)] nonfatal(_ipm, *margs) args = '-m', 'mark', '--mark', str(port), '-j', chain else: @@ -104,10 +104,10 @@ def _ipm(*args): if user is not None or group is not None: margs = ['-D', 'OUTPUT', '-m', 'owner'] if user is not None: - margs.append('--uid-owner', str(user)) + margs += ['--uid-owner', str(user)] if group is not None: - margs.append('--gid-owner', str(group)) - margs = args.append('-j', 'MARK', '--set-mark', str(port)) + margs += ['--gid-owner', str(group)] + margs += ['-j', 'MARK', '--set-mark', str(port)] nonfatal(_ipm, *margs) args = '-m', 'mark', '--mark', str(port), '-j', chain diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index e12943c9b..b3d5fca29 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -134,7 +134,7 @@ def _ipt_proto_ports(proto, fport, lport): divert_chain = 'sshuttle-d-%s' % port # basic cleanup/setup of chains - self.restore_firewall(port, family, udp, user) + self.restore_firewall(port, family, udp, user, group) _ipt('-N', mark_chain) _ipt('-F', mark_chain) From 998e5c58491a6bc88d03682e680b65381f59f7f4 Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Fri, 4 Aug 2023 19:51:03 +0700 Subject: [PATCH 104/275] Fix tests --- tests/client/test_firewall.py | 6 ++++-- tests/client/test_methods_nat.py | 7 +++++-- tests/client/test_methods_pf.py | 19 ++++++++++++++----- tests/client/test_methods_tproxy.py | 6 ++++-- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 740432869..6a3b4db17 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -145,6 +145,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None, + None, '0x01'), call().setup_firewall( 1025, 1027, @@ -154,7 +155,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None, + None, '0x01'), - call().restore_firewall(1024, AF_INET6, True, None), - call().restore_firewall(1025, AF_INET, True, None), + call().restore_firewall(1024, AF_INET6, True, None, None), + call().restore_firewall(1025, AF_INET, True, None, None), ] diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index 61b74b5de..218b629d0 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -101,6 +101,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], False, None, + None, '0x01') assert mock_ipt_chain_exists.mock_calls == [ @@ -142,6 +143,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None, + None, '0x01') assert str(excinfo.value) == 'UDP not supported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] @@ -155,6 +157,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], False, None, + None, '0x01') assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'nat', 'sshuttle-1025') @@ -182,7 +185,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() - method.restore_firewall(1025, AF_INET, False, None) + method.restore_firewall(1025, AF_INET, False, None, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'nat', 'sshuttle-1025') ] @@ -197,7 +200,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() - method.restore_firewall(1025, AF_INET6, False, None) + method.restore_firewall(1025, AF_INET6, False, None,) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'nat', 'sshuttle-1025') ] diff --git a/tests/client/test_methods_pf.py b/tests/client/test_methods_pf.py index ea7416a0e..46de24c74 100644 --- a/tests/client/test_methods_pf.py +++ b/tests/client/test_methods_pf.py @@ -187,6 +187,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, + None, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), @@ -227,6 +228,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, + None, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] @@ -241,6 +243,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, + None, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), @@ -270,7 +273,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): mock_ioctl.reset_mock() mock_pfctl.reset_mock() - method.restore_firewall(1025, AF_INET, False, None) + method.restore_firewall(1025, AF_INET, False, None, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), @@ -302,6 +305,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, + None, '0x01') assert mock_pfctl.mock_calls == [ @@ -335,6 +339,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, + None, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] @@ -349,6 +354,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, + None, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), @@ -376,8 +382,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, mock_ioctl.reset_mock() mock_pfctl.reset_mock() - method.restore_firewall(1025, AF_INET, False, None) - method.restore_firewall(1024, AF_INET6, False, None) + method.restore_firewall(1025, AF_INET, False, None, None) + method.restore_firewall(1024, AF_INET6, False, None, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), @@ -408,6 +414,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, + None, '0x01') assert mock_ioctl.mock_calls == [ @@ -445,6 +452,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, + None, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] @@ -459,6 +467,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, + None, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), @@ -484,8 +493,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): mock_ioctl.reset_mock() mock_pfctl.reset_mock() - method.restore_firewall(1025, AF_INET, False, None) - method.restore_firewall(1024, AF_INET6, False, None) + method.restore_firewall(1025, AF_INET, False, None, None) + method.restore_firewall(1024, AF_INET6, False, None, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), diff --git a/tests/client/test_methods_tproxy.py b/tests/client/test_methods_tproxy.py index d7bc33f5b..b1e72ff5a 100644 --- a/tests/client/test_methods_tproxy.py +++ b/tests/client/test_methods_tproxy.py @@ -98,6 +98,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], True, None, + None, '0x01') assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1024'), @@ -172,7 +173,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() - method.restore_firewall(1025, AF_INET6, True, None) + method.restore_firewall(1025, AF_INET6, True, None, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', 'sshuttle-t-1025'), @@ -201,6 +202,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, + None, '0x01') assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), @@ -270,7 +272,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() - method.restore_firewall(1025, AF_INET, True, None) + method.restore_firewall(1025, AF_INET, True, None, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'), From 5b9f438d42c389aaf92f62ff58522c2f74f92b6b Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Fri, 4 Aug 2023 19:59:33 +0700 Subject: [PATCH 105/275] Fix tests --- tests/client/test_firewall.py | 2 +- tests/client/test_methods_nat.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 6a3b4db17..f82ddfa5d 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -19,7 +19,7 @@ def setup_daemon(): {inet},1.2.3.33 {inet6},2404:6800:4004:80c::33 PORTS 1024,1025,1026,1027 -GO 1 - 0x01 12345 +GO 1 - - 0x01 12345 HOST 1.2.3.3,existing """.format(inet=AF_INET, inet6=AF_INET6)) stdout = Mock() diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index 218b629d0..fa969aa83 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -119,14 +119,14 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT', '--dest', u'2404:6800:4004:80c::33', '-p', 'udp', '--dport', '53', '--to-ports', '1026'), - call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', - '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-p', 'tcp', '--dport', '80:80'), call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT', '--dest', u'2404:6800:4004:80c::/64', '-p', 'tcp', - '--to-ports', '1024') + '--to-ports', '1024'), + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', + '-m', 'addrtype', '--dst-type', 'LOCAL') ] mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() @@ -174,13 +174,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', '--dest', u'1.2.3.33', '-p', 'udp', '--dport', '53', '--to-ports', '1027'), - call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', - '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', '--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000', - '--to-ports', '1025') + '--to-ports', '1025'), + call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', + '-m', 'addrtype', '--dst-type', 'LOCAL'), ] mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() @@ -200,7 +200,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): mock_ipt_chain_exists.reset_mock() mock_ipt.reset_mock() - method.restore_firewall(1025, AF_INET6, False, None,) + method.restore_firewall(1025, AF_INET6, False, None, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'nat', 'sshuttle-1025') ] From 3abc3d2a1a12d8345502e6dd12b76728d7fcbcda Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Mon, 7 Aug 2023 09:09:41 +0700 Subject: [PATCH 106/275] Fix lint issues --- sshuttle/client.py | 2 +- sshuttle/methods/nat.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 5d1d990de..5dbb7ad3d 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -371,7 +371,7 @@ def start(self): if self.group is None: group = b'-' elif isinstance(self.group, str): - group = bytes(self.group, 'utf-8') + group = bytes(self.group, 'utf-8') else: group = b'%d' % self.group self.pfile.write(b'GO %d %s %s %s %d\n' % diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index bd878b11c..4da1a8354 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -74,7 +74,7 @@ def _ipm(*args): _ipt('-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet, swidth), *(tcp_ports + ('--to-ports', str(port)))) - + # Don't route any remaining local traffic through sshuttle. _ipt('-A', chain, '-j', 'RETURN', '-m', 'addrtype', From 9f718e8632da548e96ab1bac9130c45463d46fc8 Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Mon, 7 Aug 2023 09:19:31 +0700 Subject: [PATCH 107/275] Fix typo --- sshuttle/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 5dbb7ad3d..c652f6502 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -845,7 +845,7 @@ def main(listenip_v6, listenip_v4, try: group = getgrnam(group).gr_gid except KeyError: - raise Fatal("User %s does not exist." % user) + raise Fatal("Group %s does not exist." % user) required.group = False if group is None else True if not required.ipv6 and len(subnets_v6) > 0: From 3c3f5de6728ebdea818d5cef611d20979c8170b4 Mon Sep 17 00:00:00 2001 From: Alexander Naumov Date: Mon, 7 Aug 2023 07:48:05 +0200 Subject: [PATCH 108/275] sshuttle is also avaliable on OpenBSD --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 05ec05501..0c915fc8c 100644 --- a/README.rst +++ b/README.rst @@ -75,6 +75,10 @@ Obtaining sshuttle # pkg pkg install py36-sshuttle +- OpenBSD:: + + pkg_add sshuttle + - macOS, via MacPorts:: sudo port selfupdate From 0ddebdeee66eeaaaf521efc1eba2c718f89bc936 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Wed, 9 Aug 2023 09:50:34 +0800 Subject: [PATCH 109/275] Add support for SSHUTTLE_ARGS environment variable --- docs/manpage.rst | 12 ++++++++++++ sshuttle/cmdline.py | 11 ++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index fa1106feb..2053eee37 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -321,6 +321,18 @@ annotations. For example:: 192.168.63.0/24 +Environment Variable +-------------------- + +You can specify command line options with the `SSHUTTLE_ARGS` environment +variable. If a given option is defined in both the environment variable and +command line, the value on the command line will take precedence. + +For example:: + + SSHUTTLE_ARGS="-e 'ssh -v' --dns" sshuttle -r example.com 0/0 + + Examples -------- diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index eaca961d8..548081598 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -1,5 +1,8 @@ +import os import re +import shlex import socket +import sys import sshuttle.helpers as helpers import sshuttle.client as client import sshuttle.firewall as firewall @@ -11,7 +14,13 @@ def main(): - opt = parser.parse_args() + if 'SSHUTTLE_ARGS' in os.environ: + env_args = shlex.split(os.environ['SSHUTTLE_ARGS']) + else: + env_args = [] + args = [*env_args, *sys.argv[1:]] + + opt = parser.parse_args(args) if opt.sudoers_no_modify: # sudoers() calls exit() when it completes From cec87a5341fea42b728d8cc6dd955007d0cc3d3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:13:16 +0000 Subject: [PATCH 110/275] Bump furo from 2023.7.26 to 2023.8.17 Bumps [furo](https://github.com/pradyunsg/furo) from 2023.7.26 to 2023.8.17. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.07.26...2023.08.17) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6e9afcfe..a52e0edeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2023.7.26 +furo==2023.8.17 From c2ddaa0bcfbb0f51e794057ee280ad1450d897bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:00:09 +0000 Subject: [PATCH 111/275] Bump furo from 2023.8.17 to 2023.8.19 Bumps [furo](https://github.com/pradyunsg/furo) from 2023.8.17 to 2023.8.19. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.08.17...2023.08.19) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a52e0edeb..67079238d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2023.8.17 +furo==2023.8.19 From 399d389af6f2fa099356c4703524878f73c39380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:41:58 +0000 Subject: [PATCH 112/275] Bump pytest from 7.4.0 to 7.4.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.0...7.4.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index e2344b4ce..d03a6d29c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.4.0 +pytest==7.4.1 pytest-cov==4.1.0 flake8==5.0.4 pyflakes==2.5.0 From 3e804646261e1093ff5e36fca1ffb6ef857ec2e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:59:06 +0000 Subject: [PATCH 113/275] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7d89bb851..dc4cae299 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index de573f810..5e9a27306 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,7 +20,7 @@ jobs: python-version: ["3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: From 031fb4d053c465cafa03c1b7ccdb0e3f549bace0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:45:37 +0000 Subject: [PATCH 114/275] Bump pytest from 7.4.1 to 7.4.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.1 to 7.4.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.1...7.4.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index d03a6d29c..cb1dbd335 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.4.1 +pytest==7.4.2 pytest-cov==4.1.0 flake8==5.0.4 pyflakes==2.5.0 From efb7d1f6cc391701b837d75ad8be2634cb713767 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:37:50 +0000 Subject: [PATCH 115/275] Bump flake8 from 5.0.4 to 6.1.0 Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.4 to 6.1.0. - [Commits](https://github.com/pycqa/flake8/compare/5.0.4...6.1.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index cb1dbd335..7d486524c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,6 @@ -r requirements.txt pytest==7.4.2 pytest-cov==4.1.0 -flake8==5.0.4 +flake8==6.1.0 pyflakes==2.5.0 bump2version==1.0.1 From 6f70519dc1225d15693af990406cc5278a8b3971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:37:54 +0000 Subject: [PATCH 116/275] Bump pyflakes from 2.5.0 to 3.1.0 Bumps [pyflakes](https://github.com/PyCQA/pyflakes) from 2.5.0 to 3.1.0. - [Changelog](https://github.com/PyCQA/pyflakes/blob/main/NEWS.rst) - [Commits](https://github.com/PyCQA/pyflakes/compare/2.5.0...3.1.0) --- updated-dependencies: - dependency-name: pyflakes dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 7d486524c..6a082c1ab 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,5 +2,5 @@ pytest==7.4.2 pytest-cov==4.1.0 flake8==6.1.0 -pyflakes==2.5.0 +pyflakes==3.1.0 bump2version==1.0.1 From 670cc363bad1366a270636dbaeb7543d06622337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:05:47 +0000 Subject: [PATCH 117/275] Bump furo from 2023.8.19 to 2023.9.10 Bumps [furo](https://github.com/pradyunsg/furo) from 2023.8.19 to 2023.9.10. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.08.19...2023.09.10) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67079238d..d1315fc27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2023.8.19 +furo==2023.9.10 From 794b14eaacda22edc5bb5c0bc7f71d62f50260af Mon Sep 17 00:00:00 2001 From: Jose M Perez Date: Tue, 3 Oct 2023 01:27:44 +0200 Subject: [PATCH 118/275] tproxy: Apply DNS rules first Having --dst-type LOCAL rules before DNS ones forces the usage of a dnsmasq-like program to retrigger DNS requests directed locally because they are fast-tracked through the firewall and ignored by sshuttle. As dns options documentation state that they capture the requests no matter the server, and other methods and older versions behave consistently, change the iptables rules to apply DNS ones first. --- sshuttle/methods/tproxy.py | 22 +++++++++++----------- tests/client/test_methods_tproxy.py | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index b3d5fca29..84eea3ff0 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -145,8 +145,18 @@ def _ipt_proto_ports(proto, fport, lport): _ipt('-I', 'OUTPUT', '1', '-j', mark_chain) _ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain) + for _, ip in [i for i in nslist if i[0] == family]: + _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark, + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53') + _ipt('-A', tproxy_chain, '-j', 'TPROXY', + '--tproxy-mark', tmark, + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53', + '--on-port', str(dnsport)) + # Don't have packets sent to any of our local IP addresses go - # through the tproxy or mark chains. + # through the tproxy or mark chains (except DNS ones). # # Without this fix, if a large subnet is redirected through # sshuttle (i.e., 0/0), then the user may be unable to receive @@ -169,16 +179,6 @@ def _ipt_proto_ports(proto, fport, lport): _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'udp', '-p', 'udp') - for _, ip in [i for i in nslist if i[0] == family]: - _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark, - '--dest', '%s/32' % ip, - '-m', 'udp', '-p', 'udp', '--dport', '53') - _ipt('-A', tproxy_chain, '-j', 'TPROXY', - '--tproxy-mark', tmark, - '--dest', '%s/32' % ip, - '-m', 'udp', '-p', 'udp', '--dport', '53', - '--on-port', str(dnsport)) - for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): tcp_ports = ('-p', 'tcp') diff --git a/tests/client/test_methods_tproxy.py b/tests/client/test_methods_tproxy.py index b1e72ff5a..44184e54a 100644 --- a/tests/client/test_methods_tproxy.py +++ b/tests/client/test_methods_tproxy.py @@ -123,6 +123,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'), + call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', + '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32', + '-m', 'udp', '-p', 'udp', '--dport', '53'), + call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', + '--tproxy-mark', '0x01', + '--dest', u'2404:6800:4004:80c::33/32', + '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', @@ -134,13 +141,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), - call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', - '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32', - '-m', 'udp', '-p', 'udp', '--dport', '53'), - call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', - '--tproxy-mark', '0x01', - '--dest', u'2404:6800:4004:80c::33/32', - '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'), @@ -227,6 +227,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'), + call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', + '--set-mark', '0x01', '--dest', u'1.2.3.33/32', + '-m', 'udp', '-p', 'udp', '--dport', '53'), + call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', + '--tproxy-mark', '0x01', '--dest', u'1.2.3.33/32', + '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', @@ -238,12 +244,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt): '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), - call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', - '--set-mark', '0x01', '--dest', u'1.2.3.33/32', - '-m', 'udp', '-p', 'udp', '--dport', '53'), - call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', - '--tproxy-mark', '0x01', '--dest', u'1.2.3.33/32', - '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', '--dport', '80:80'), From e53c0df411e0d19c4a6905831e3d70a157170e4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:54:44 +0000 Subject: [PATCH 119/275] Bump pytest from 7.4.2 to 7.4.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.2 to 7.4.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.2...7.4.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6a082c1ab..b079ec756 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.4.2 +pytest==7.4.3 pytest-cov==4.1.0 flake8==6.1.0 pyflakes==3.1.0 From 67b4499c52cc12834de421d3fd5667ac21ec827f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:09:50 +0000 Subject: [PATCH 120/275] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5e9a27306..166d9d8a7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From ef804e7cdb32e65e2ceb87d022096dcbb71bf884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:07:17 +0000 Subject: [PATCH 121/275] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dc4cae299..37d7f43b4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 59b6777f01fa41ddc71b049426a60131f3ce8933 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:51:41 +0000 Subject: [PATCH 122/275] Bump pytest from 7.4.3 to 7.4.4 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 7.4.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.3...7.4.4) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index b079ec756..71fe081a6 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.4.3 +pytest==7.4.4 pytest-cov==4.1.0 flake8==6.1.0 pyflakes==3.1.0 From b4e4680ef49c4062e8603cd6e86e93f8520f1d41 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Mon, 1 Jan 2024 16:01:39 -0500 Subject: [PATCH 123/275] Workaround when sudo prints text to standard out When we use sudo and start the firewall process, we should be able to read standard in and find the string "READY". However, some administrators use a wrapper around sudo to print warning messages (instead of sudo's lecture feature) to standard out. This commit reads up to 100 lines looking for "READY" instead of expecting it on the first line. I believe this should fix issue #916. --- sshuttle/client.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index c652f6502..2b0bd18d2 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -302,10 +302,28 @@ def setup(): '%r returned %d' % (self.argv, rv)) continue + # Normally, READY will be the first text on the first + # line. However, if an administrator replaced sudo with a + # shell script that echos a message to stdout and then + # runs sudo, READY won't be on the first line. To + # workaround this problem, we read a limited number of + # lines until we encounter "READY". Store all of the text + # we skipped in case we need it for an error message. + # + # A proper way to print a sudo warning message is to use + # sudo's lecture feature. sshuttle works correctly without + # this hack if sudo's lecture feature is used instead. + skipped_text = line + for i in range(100): + if line[0:5] == b'READY': + break + line = self.pfile.readline() + skipped_text += line + if line[0:5] != b'READY': debug1('Unable to start firewall manager. ' 'Expected READY, got %r. ' - 'Command=%r' % (line, self.argv)) + 'Command=%r' % (skipped_text, self.argv)) continue method_name = line[6:-1] From a604d107ef52a069b8e97ac15ff668ae2aea293b Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Thu, 4 Jan 2024 15:49:43 -0500 Subject: [PATCH 124/275] Keep terminal in a sane state when sudo use_pty is used. This fixes #909 and is an alternative to the #922 pull request. When sudo's use_pty is used with sshuttle, it causes issues with the terminal. Pull request #712 contains some fixes for this problem. However, when sshuttle is run with the --daemon option, it left the user's terminal in a non-sane state. The problem appears to be related to a socketpair that the firewall uses for communication. By setting it up slightly differently (see changes to client.py and firewall.py), the terminal state is no longer disrupted. This commit also changes line endings of the printed messages from \r\n to \n. This undoes a change introduced by pull request #712 and is no longer needed. --- sshuttle/assembler.py | 2 +- sshuttle/client.py | 3 ++- sshuttle/firewall.py | 5 ----- sshuttle/helpers.py | 9 +-------- tests/client/test_helpers.py | 24 ++++++++++++------------ 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 6eb88002d..3cffdee97 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -18,7 +18,7 @@ name = name.decode("ASCII") nbytes = int(sys.stdin.readline()) if verbosity >= 2: - sys.stderr.write(' s: assembling %r (%d bytes)\r\n' + sys.stderr.write(' s: assembling %r (%d bytes)\n' % (name, nbytes)) content = z.decompress(sys.stdin.read(nbytes)) diff --git a/sshuttle/client.py b/sshuttle/client.py index 2b0bd18d2..29c3dfad8 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -278,7 +278,8 @@ def setup(): try: debug1("Starting firewall manager with command: %r" % argv) - self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) + self.p = ssubprocess.Popen(argv, stdout=s1, stdin=s1, + preexec_fn=setup) # No env: Talking to `FirewallClient.start`, which has no i18n. except OSError as e: # This exception will occur if the program isn't diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 60662b9dd..6cc1bd983 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -110,11 +110,6 @@ def setup_daemon(): # setsid() fails if sudo is configured with the use_pty option. pass - # because of limitations of the 'su' command, the *real* stdin/stdout - # are both attached to stdout initially. Clone stdout into stdin so we - # can read from it. - os.dup2(1, 0) - return sys.stdin, sys.stdout diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 8ff536a4d..2d747e442 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -22,14 +22,7 @@ def log(s): prefix = logprefix s = s.rstrip("\n") for line in s.split("\n"): - # We output with \r\n instead of \n because when we use - # sudo with the use_pty option, the firewall process, the - # other processes printing to the terminal will have the - # \n move to the next line, but they will fail to reset - # cursor to the beginning of the line. Printing output - # with \r\n endings fixes that problem and does not appear - # to cause problems elsewhere. - sys.stderr.write(prefix + line + "\r\n") + sys.stderr.write(prefix + line + "\n") prefix = " " sys.stderr.flush() except IOError: diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py index 1e8d6bb91..ca1aba334 100644 --- a/tests/client/test_helpers.py +++ b/tests/client/test_helpers.py @@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\r\n'), + call.write('prefix: message\n'), call.flush(), - call.write('prefix: abc\r\n'), + call.write('prefix: abc\n'), call.flush(), - call.write('prefix: message 1\r\n'), + call.write('prefix: message 1\n'), call.flush(), - call.write('prefix: message 2\r\n'), - call.write(' line2\r\n'), - call.write(' line3\r\n'), + call.write('prefix: message 2\n'), + call.write(' line2\n'), + call.write(' line3\n'), call.flush(), - call.write('prefix: message 3\r\n'), - call.write(' line2\r\n'), - call.write(' line3\r\n'), + call.write('prefix: message 3\n'), + call.write(' line2\n'), + call.write(' line3\n'), call.flush(), ] @@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\r\n'), + call.write('prefix: message\n'), call.flush(), ] @@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\r\n'), + call.write('prefix: message\n'), call.flush(), ] @@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout): call.flush(), ] assert mock_stderr.mock_calls == [ - call.write('prefix: message\r\n'), + call.write('prefix: message\n'), call.flush(), ] From 152c14c0792737445e05af3a194aca7e847ae9aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:50:46 +0000 Subject: [PATCH 125/275] Bump pytest from 7.4.4 to 8.0.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.4 to 8.0.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.4...8.0.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 71fe081a6..d0b86a981 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==7.4.4 +pytest==8.0.0 pytest-cov==4.1.0 flake8==6.1.0 pyflakes==3.1.0 From e4ae714cf82a82ff274af03706f87ee3eb8023e8 Mon Sep 17 00:00:00 2001 From: Benjamin Barthe Date: Mon, 29 Jan 2024 11:34:22 +0100 Subject: [PATCH 126/275] fixing a tiny typo --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 912d84695..524f3ba71 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -11,7 +11,7 @@ Forward all traffic:: sshuttle -r username@sshserver 0.0.0.0/0 - Use the :option:`sshuttle -r` parameter to specify a remote server. - One some systems, you may also need to use the :option:`sshuttle -x` + On some systems, you may also need to use the :option:`sshuttle -x` parameter to exclude sshserver or sshserver:22 so that your local machine can communicate directly to sshserver without it being redirected by sshuttle. From 9b831499d7a0bc32b61c7c15ba1d2bd76a321fd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:57:35 +0000 Subject: [PATCH 127/275] Bump furo from 2023.9.10 to 2024.1.29 Bumps [furo](https://github.com/pradyunsg/furo) from 2023.9.10 to 2024.1.29. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2023.09.10...2024.01.29) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d1315fc27..5b8f3eb1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2023.9.10 +furo==2024.1.29 From b2a29d3b229053b2f0d6e0bdb2a35be70564aa10 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 31 Jan 2024 07:18:24 +0100 Subject: [PATCH 128/275] Allow flake8 to determine the version of pyflakes --- requirements-tests.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index d0b86a981..6298a0623 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,5 @@ -r requirements.txt pytest==8.0.0 pytest-cov==4.1.0 -flake8==6.1.0 -pyflakes==3.1.0 +flake8==7.0.0 bump2version==1.0.1 From 32d005445544f3dc66b64a8f97ddc57b3b27bacc Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 30 Jan 2024 15:41:42 +0100 Subject: [PATCH 129/275] Fix typos discovered by codespell https://pypi.org/project/codespell --- docs/usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 524f3ba71..fb0db0b80 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -85,9 +85,9 @@ To print a sudo configuration file and see a suggested way to install it, run:: A custom user or group can be set with the :option:`sshuttle --sudoers-no-modify --sudoers-user {user_descriptor}` option. Valid values for this vary based on how your system is configured. -Values such as usernames, groups pre-pended with `%` and sudoers user +Values such as usernames, groups prepended with `%` and sudoers user aliases will work. See the sudoers manual for more information on valid -user specif actions. The option must be used with `--sudoers-no-modify`:: +user-specific actions. The option must be used with `--sudoers-no-modify`:: sshuttle --sudoers-no-modify --sudoers-user mike sshuttle --sudoers-no-modify --sudoers-user %sudo From 5c479220a75fbe390d8284285baf40ef13256011 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 30 Jan 2024 15:49:56 +0100 Subject: [PATCH 130/275] Update usage.rst --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index fb0db0b80..c535884c5 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -87,7 +87,7 @@ A custom user or group can be set with the option. Valid values for this vary based on how your system is configured. Values such as usernames, groups prepended with `%` and sudoers user aliases will work. See the sudoers manual for more information on valid -user-specific actions. The option must be used with `--sudoers-no-modify`:: +user-specified actions. The option must be used with `--sudoers-no-modify`:: sshuttle --sudoers-no-modify --sudoers-user mike sshuttle --sudoers-no-modify --sudoers-user %sudo From 89bd3fc2f3b037d413ba505dd86f2978131e3089 Mon Sep 17 00:00:00 2001 From: Pouria Mousavizadeh Tehrani Date: Fri, 26 Jan 2024 11:48:23 +0330 Subject: [PATCH 131/275] Update FreeBSD Installation README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0c915fc8c..47a14f990 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Obtaining sshuttle # ports cd /usr/ports/net/py-sshuttle && make install clean # pkg - pkg install py36-sshuttle + pkg install py39-sshuttle - OpenBSD:: From dd037dd8ef6f0ff648361b06926cd2aefa936a76 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 19 Feb 2024 11:02:04 +1100 Subject: [PATCH 132/275] Add experimental peotry and nix flake stuff --- .envrc | 1 + .gitignore | 4 +- flake.lock | 175 +++++++++++++++++++++++++++++++++ flake.nix | 41 ++++++++ poetry.lock | 260 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 24 +++++ setup.cfg | 2 + 7 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..a5dbbcba7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . diff --git a/.gitignore b/.gitignore index 022142177..2ecd9c703 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ /.redo /.pytest_cache/ /.python-version -.vscode/ +/.direnv/ +/result +/.vscode/ diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..ac93bf25c --- /dev/null +++ b/flake.lock @@ -0,0 +1,175 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1698974481, + "narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "4bb5e752616262457bc7ca5882192a564c0472d2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1708161998, + "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "84d981bae8b5e783b3b548de505b22880559515f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "poetry2nix": { + "inputs": { + "flake-utils": "flake-utils_2", + "nix-github-actions": "nix-github-actions", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_3", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1708175019, + "narHash": "sha256-B7wY2pNrLc3X9uYRo1LUmVzI6oH6fX8oi+96GdUpayU=", + "owner": "nix-community", + "repo": "poetry2nix", + "rev": "403d923ea8e2e6cedce3a0f04a9394c4244cb806", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "poetry2nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "poetry2nix": "poetry2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "id": "systems", + "type": "indirect" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1699786194, + "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..b6b2db8a8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = + "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."; + + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; + inputs.poetry2nix = { + url = "github:nix-community/poetry2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, flake-utils, poetry2nix }: + flake-utils.lib.eachDefaultSystem (system: + let + p2n = import poetry2nix { inherit pkgs; }; + overrides = p2n.defaultPoetryOverrides.extend (self: super: { + bump2version = super.bump2version.overridePythonAttrs (old: { + buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ]; + }); + }); + + poetry_env = p2n.mkPoetryEnv { + python = pkgs.python3; + projectDir = self; + inherit overrides; + }; + poetry_app = p2n.mkPoetryApplication { + python = pkgs.python3; + projectDir = self; + inherit overrides; + }; + pkgs = nixpkgs.legacyPackages.${system}; + in { + packages = { + sshuttle = poetry_app; + default = self.packages.${system}.sshuttle; + }; + devShells.default = + pkgs.mkShell { packages = [ pkgs.poetry poetry_env ]; }; + }); +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..b56abc86c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,260 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "bump2version" +version = "1.0.1" +description = "Version-bump your software with a single command!" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, + {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.4.1" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pytest" +version = "8.0.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "edb2865bbab00897a40eda0c20c65efbac00f033746cdda0c2119fc33a361cff" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..07a99b537 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "sshuttle" +version = "1.1.1" +description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling." +authors = ["Brian May "] +license = "LGPL-2.1" +readme = "README.rst" + +[tool.poetry.dependencies] +python = "^3.10" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.1" +pytest-cov = "^4.1.0" +flake8 = "^7.0.0" +pyflakes = "^3.2.0" +bump2version = "^1.0.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +sshuttle = "sshuttle.cmdline:main" diff --git a/setup.cfg b/setup.cfg index 59a9b1231..389152023 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ current_version = 1.1.1 [bumpversion:file:setup.py] +[bumpversion:file:pyproject.toml] + [bumpversion:file:sshuttle/version.py] [aliases] From e6563f2c3978742488002f0e5c235cd68f50521c Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 19 Feb 2024 11:18:31 +1100 Subject: [PATCH 133/275] Add twine to poetry packages --- flake.nix | 1 + poetry.lock | 620 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 3 files changed, 606 insertions(+), 16 deletions(-) diff --git a/flake.nix b/flake.nix index b6b2db8a8..78abd53db 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,7 @@ let p2n = import poetry2nix { inherit pkgs; }; overrides = p2n.defaultPoetryOverrides.extend (self: super: { + nh3 = super.nh3.override { preferWheel = true; }; bump2version = super.bump2version.overridePythonAttrs (old: { buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ]; }); diff --git a/poetry.lock b/poetry.lock index b56abc86c..cc5886016 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "bump2version" version = "1.0.1" description = "Version-bump your software with a single command!" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -12,11 +11,184 @@ files = [ {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, ] +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -28,7 +200,6 @@ files = [ name = "coverage" version = "7.4.1" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -92,11 +263,75 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "42.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"}, + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"}, + {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"}, + {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"}, + {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"}, + {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"}, + {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"}, + {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "docutils" +version = "0.20.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -111,7 +346,6 @@ test = ["pytest (>=6)"] name = "flake8" version = "7.0.0" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -124,11 +358,40 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.2.0,<3.3.0" +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.0.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -136,11 +399,90 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jaraco-classes" +version = "3.3.1" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"}, + {file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "keyring" +version = "24.3.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"}, + {file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab (>=1.1.0)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -148,11 +490,57 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + +[[package]] +name = "nh3" +version = "0.2.15" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"}, + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"}, + {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"}, + {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"}, + {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, +] + [[package]] name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -160,11 +548,24 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pkginfo" +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov"] + [[package]] name = "pluggy" version = "1.4.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -180,7 +581,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pycodestyle" version = "2.11.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -188,11 +588,21 @@ files = [ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pyflakes" version = "3.2.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -200,11 +610,25 @@ files = [ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.0.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -227,7 +651,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -242,11 +665,122 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "readme-renderer" +version = "42.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "readme_renderer-42.0-py3-none-any.whl", hash = "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d"}, + {file = "readme_renderer-42.0.tar.gz", hash = "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1"}, +] + +[package.dependencies] +docutils = ">=0.13.1" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.7.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -254,7 +788,61 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "twine" +version = "5.0.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "twine-5.0.0-py3-none-any.whl", hash = "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0"}, + {file = "twine-5.0.0.tar.gz", hash = "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4"}, +] + +[package.dependencies] +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.8.1" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "zipp" +version = "3.17.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "edb2865bbab00897a40eda0c20c65efbac00f033746cdda0c2119fc33a361cff" +content-hash = "e089c5db68017b943468ee1860df31baf7ec88633ce8aaaa25e82df84ed59a4d" diff --git a/pyproject.toml b/pyproject.toml index 07a99b537..8441f2739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ pytest-cov = "^4.1.0" flake8 = "^7.0.0" pyflakes = "^3.2.0" bump2version = "^1.0.1" +twine = "^5.0.0" [build-system] requires = ["poetry-core"] From fd424c5c5558653876a896cf7f196f9a562685e8 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 19 Feb 2024 11:21:39 +1100 Subject: [PATCH 134/275] =?UTF-8?q?Bump=20version:=201.1.1=20=E2=86=92=201?= =?UTF-8?q?.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- setup.cfg | 2 +- setup.py | 2 +- sshuttle/version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8441f2739..807e15942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sshuttle" -version = "1.1.1" +version = "1.1.2" description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling." authors = ["Brian May "] license = "LGPL-2.1" diff --git a/setup.cfg b/setup.cfg index 389152023..494952755 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.1 +current_version = 1.1.2 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 4d4925764..ebc65d16e 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="sshuttle", - version='1.1.1', + version='1.1.2', url='https://github.com/sshuttle/sshuttle', author='Brian May', author_email='brian@linuxpenguins.xyz', diff --git a/sshuttle/version.py b/sshuttle/version.py index b5b4f934c..706230230 100644 --- a/sshuttle/version.py +++ b/sshuttle/version.py @@ -1 +1 @@ -__version__ = version = '1.1.1' +__version__ = version = '1.1.2' From 83debdfb2102a80fc2bcacaf34d95492f8cd9153 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:30:31 +0000 Subject: [PATCH 135/275] Bump pytest from 8.0.0 to 8.0.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.0 to 8.0.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0...8.0.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6298a0623..bcaa072ec 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.0.0 +pytest==8.0.1 pytest-cov==4.1.0 flake8==7.0.0 bump2version==1.0.1 From 87bd34e09422b9dba70b83d20ff721f75481e8c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:49:36 +0000 Subject: [PATCH 136/275] Bump cryptography from 42.0.3 to 42.0.4 Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.3 to 42.0.4. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.3...42.0.4) --- updated-dependencies: - dependency-name: cryptography dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 66 ++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index cc5886016..e11979b18 100644 --- a/poetry.lock +++ b/poetry.lock @@ -265,43 +265,43 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.3" +version = "42.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"}, - {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"}, - {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"}, - {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"}, - {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"}, - {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"}, - {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"}, - {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"}, + {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, + {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, + {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, + {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, + {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, + {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, + {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, + {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, ] [package.dependencies] From 934618b603153bde8331ac260a4a5f4faa987c30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:23:26 +0000 Subject: [PATCH 137/275] Bump pytest from 8.0.1 to 8.0.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.1 to 8.0.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.1...8.0.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index bcaa072ec..ff2286271 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.0.1 +pytest==8.0.2 pytest-cov==4.1.0 flake8==7.0.0 bump2version==1.0.1 From 313ada3ff77211a85cc92de1852c34296ba1c3b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:42:01 +0000 Subject: [PATCH 138/275] Bump pytest from 8.0.1 to 8.0.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.1 to 8.0.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.1...8.0.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index e11979b18..17a1ca8c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.0.1" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, - {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] From 3d875b8ca89b76263c8931ad29fb31e002985e30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:59:49 +0000 Subject: [PATCH 139/275] Bump pytest from 8.0.2 to 8.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index ff2286271..6bd2a088f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.0.2 +pytest==8.1.0 pytest-cov==4.1.0 flake8==7.0.0 bump2version==1.0.1 From 9a4df1fdcf48ffac7cfbaa8a6efd64b1337a05e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:45:08 +0000 Subject: [PATCH 140/275] Bump pytest from 8.0.2 to 8.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6bd2a088f..c36f56aeb 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.1.0 +pytest==8.1.1 pytest-cov==4.1.0 flake8==7.0.0 bump2version==1.0.1 From afbdf8b606e0bd1259c323b48fe80d561add54cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:32:45 +0000 Subject: [PATCH 141/275] Bump pytest from 8.0.2 to 8.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 17a1ca8c3..f2f9985d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.0.2" +version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] @@ -641,11 +641,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" From 264e4d94b8b4b56105b72e18dd47aa3764e49273 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:25:08 +0000 Subject: [PATCH 142/275] Bump pytest-cov from 4.1.0 to 5.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- poetry.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index f2f9985d6..267ae4c0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -649,13 +649,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -663,7 +663,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pywin32-ctypes" @@ -845,4 +845,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e089c5db68017b943468ee1860df31baf7ec88633ce8aaaa25e82df84ed59a4d" +content-hash = "e38482dec55172a5bf913f817cbc144671935895fed8e92e8d7be7d4ba759fd4" diff --git a/pyproject.toml b/pyproject.toml index 807e15942..10001f865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.10" [tool.poetry.group.dev.dependencies] pytest = "^8.0.1" -pytest-cov = "^4.1.0" +pytest-cov = ">=4.1,<6.0" flake8 = "^7.0.0" pyflakes = "^3.2.0" bump2version = "^1.0.1" From 694a9c8a5ba7e0332915a3b204abed6e6a33c671 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:54:56 +0000 Subject: [PATCH 143/275] Bump pytest-cov from 4.1.0 to 5.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index c36f56aeb..8f4d69883 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==8.1.1 -pytest-cov==4.1.0 +pytest-cov==5.0.0 flake8==7.0.0 bump2version==1.0.1 From 116b1e22b1da11771d96c9d700b9c304651c0e61 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 8 Apr 2024 09:03:21 +1000 Subject: [PATCH 144/275] Revert "Bump cryptography from 42.0.3 to 42.0.4" This reverts commit 87bd34e09422b9dba70b83d20ff721f75481e8c2. Fixes #939 This was to be fixed in https://github.com/nix-community/poetry2nix/pull/1538, but merging that is taking longer then I might have hoped. --- poetry.lock | 66 ++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index 267ae4c0f..af35956ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -265,43 +265,43 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.4" +version = "42.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, - {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, - {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, - {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, - {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, - {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, - {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, - {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"}, + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"}, + {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"}, + {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"}, + {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"}, + {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"}, + {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"}, + {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"}, ] [package.dependencies] From 6bd3bd738ab9a7049d130ac7f768e4f2caf2d44f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:43:18 +0000 Subject: [PATCH 145/275] Bump idna from 3.6 to 3.7 Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index af35956ce..9cd2310d0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "bump2version" @@ -360,13 +360,13 @@ pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] From ea0559eaea996dc6a05da19781970e889284f013 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:42:40 +0000 Subject: [PATCH 146/275] Bump pytest from 8.1.1 to 8.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 8f4d69883..3bbb10f53 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.1.1 +pytest==8.2.0 pytest-cov==5.0.0 flake8==7.0.0 bump2version==1.0.1 From 4ba7612d904be0634cfe31bebef1cb7165306e5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:42:44 +0000 Subject: [PATCH 147/275] Bump furo from 2024.1.29 to 2024.4.27 Bumps [furo](https://github.com/pradyunsg/furo) from 2024.1.29 to 2024.4.27. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2024.01.29...2024.04.27) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b8f3eb1d..822be6b59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2024.1.29 +furo==2024.4.27 From 39a7b1b47f8696ca2f9a17a43fda03dba76718ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:28:58 +0000 Subject: [PATCH 148/275] Bump pytest from 8.1.1 to 8.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9cd2310d0..bb73f19fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -564,13 +564,13 @@ testing = ["pytest", "pytest-cov"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] @@ -641,11 +641,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" From 975d208d60f1dc98e9afbe70d5e89a5a7a0fa85f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 10:07:49 +0000 Subject: [PATCH 149/275] Bump furo from 2024.4.27 to 2024.5.6 Bumps [furo](https://github.com/pradyunsg/furo) from 2024.4.27 to 2024.5.6. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2024.04.27...2024.05.06) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 822be6b59..02f42e581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2024.4.27 +furo==2024.5.6 From c0938bc9a511c7aad5ff61f1656f4dd81a7ed8d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 10:26:08 +0000 Subject: [PATCH 150/275] Bump twine from 5.0.0 to 5.1.0 Bumps [twine](https://github.com/pypa/twine) from 5.0.0 to 5.1.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/5.0.0...5.1.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index bb73f19fa..b5e696832 100644 --- a/poetry.lock +++ b/poetry.lock @@ -790,13 +790,13 @@ files = [ [[package]] name = "twine" -version = "5.0.0" +version = "5.1.0" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" files = [ - {file = "twine-5.0.0-py3-none-any.whl", hash = "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0"}, - {file = "twine-5.0.0.tar.gz", hash = "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4"}, + {file = "twine-5.1.0-py3-none-any.whl", hash = "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"}, + {file = "twine-5.1.0.tar.gz", hash = "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"}, ] [package.dependencies] From a91e0c0470c0dcfbc2e0196b8769367a14de50d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 10:47:44 +0000 Subject: [PATCH 151/275] Bump pytest from 8.2.0 to 8.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.0 to 8.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 3bbb10f53..38b276658 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.2.0 +pytest==8.2.1 pytest-cov==5.0.0 flake8==7.0.0 bump2version==1.0.1 From 8d5e23477e1ff98a7b5d773815514ed322eed79b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 10:23:15 +0000 Subject: [PATCH 152/275] --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index b5e696832..5d1395835 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] From d660d8159b8e8b9a21790c24d061b39c8126596c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 06:01:37 +0000 Subject: [PATCH 153/275] --- updated-dependencies: - dependency-name: requests dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d1395835..60e5dcfa4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -697,13 +697,13 @@ md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, + {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, ] [package.dependencies] From a0f466a07cf85819819d6a383b917eb57aff26f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:24:16 +0000 Subject: [PATCH 154/275] Bump pytest from 8.2.1 to 8.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 38b276658..fdca933ce 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.2.1 +pytest==8.2.2 pytest-cov==5.0.0 flake8==7.0.0 bump2version==1.0.1 From efbc4d066f85b63100b6d9b64e52032e52ba6389 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:54:01 +0000 Subject: [PATCH 155/275] Bump pytest from 8.2.1 to 8.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 60e5dcfa4..f9f76df51 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] From 83c136d6e6c2551c8242b5c96126de50aee886a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:59:53 +0000 Subject: [PATCH 156/275] Bump flake8 from 7.0.0 to 7.1.0 Bumps [flake8](https://github.com/pycqa/flake8) from 7.0.0 to 7.1.0. - [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index fdca933ce..88c5561f2 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==8.2.2 pytest-cov==5.0.0 -flake8==7.0.0 +flake8==7.1.0 bump2version==1.0.1 From 4ccf5286641d456043bc79f5d2f51b5193e8cb0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:53:06 +0000 Subject: [PATCH 157/275] Bump urllib3 from 2.2.1 to 2.2.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index f9f76df51..0df9a4392 100644 --- a/poetry.lock +++ b/poetry.lock @@ -812,13 +812,13 @@ urllib3 = ">=1.26.0" [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] From cc38cc2def9f6f0cb2a40ce039bdf8c23990e3d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:36:42 +0000 Subject: [PATCH 158/275] Bump flake8 from 7.0.0 to 7.1.0 Bumps [flake8](https://github.com/pycqa/flake8) from 7.0.0 to 7.1.0. - [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0df9a4392..1965dca74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -344,18 +344,18 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "7.0.0" +version = "7.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, + {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, + {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" +pycodestyle = ">=2.12.0,<2.13.0" pyflakes = ">=3.2.0,<3.3.0" [[package]] @@ -579,13 +579,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" -version = "2.11.1" +version = "2.12.0" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, + {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, + {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, ] [[package]] From bdf2797b7411380df74bb4eb7cf21f1bf3bbe226 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:23:38 +0000 Subject: [PATCH 159/275] Bump twine from 5.1.0 to 5.1.1 Bumps [twine](https://github.com/pypa/twine) from 5.1.0 to 5.1.1. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/5.1.0...v5.1.1) --- updated-dependencies: - dependency-name: twine dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1965dca74..8c896dd88 100644 --- a/poetry.lock +++ b/poetry.lock @@ -790,19 +790,19 @@ files = [ [[package]] name = "twine" -version = "5.1.0" +version = "5.1.1" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" files = [ - {file = "twine-5.1.0-py3-none-any.whl", hash = "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"}, - {file = "twine-5.1.0.tar.gz", hash = "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"}, + {file = "twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997"}, + {file = "twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db"}, ] [package.dependencies] importlib-metadata = ">=3.6" keyring = ">=15.1" -pkginfo = ">=1.8.1" +pkginfo = ">=1.8.1,<1.11" readme-renderer = ">=35.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" From 6cdae8c3e5176d0dc194de79ed55a56e8d42a4e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 01:32:50 +0000 Subject: [PATCH 160/275] Bump certifi from 2024.2.2 to 2024.7.4 Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8c896dd88..31ea0501e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "bump2version" @@ -13,13 +13,13 @@ files = [ [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] From 348f0eb65375f5a5ccfe051ff13f9a36cc3dcb20 Mon Sep 17 00:00:00 2001 From: Samir Aguiar Date: Mon, 1 Jul 2024 23:42:51 -0300 Subject: [PATCH 161/275] Add support for non-compliant ssh wrappers ssh wrappers like teleport's tsh do not correctly interpret the double dash as an argument delimiter and will not work properly with sshuttle. This PR adds a new command line switch to handle these cases by not adding the delimiter. Fixes #599 --- docs/manpage.rst | 7 +++++++ sshuttle/client.py | 7 ++++--- sshuttle/cmdline.py | 1 + sshuttle/options.py | 8 ++++++++ sshuttle/ssh.py | 14 ++++++++------ 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 2053eee37..b860dde4b 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -181,6 +181,13 @@ Options in a non-standard location or you want to provide extra options to the ssh command, for example, ``-e 'ssh -v'``. +.. option:: --no-cmd-delimiter + + Do not add a double dash (--) delimiter before invoking Python on + the remote host. This option is useful when the ssh command used + to connect is a custom command that does not interpret this + delimiter correctly. + .. option:: --seed-hosts A comma-separated list of hostnames to use to diff --git a/sshuttle/client.py b/sshuttle/client.py index 29c3dfad8..730046b3e 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -539,7 +539,7 @@ def ondns(listener, method, mux, handlers): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, latency_buffer_size, dns_listener, seed_hosts, auto_hosts, auto_nets, daemon, - to_nameserver): + to_nameserver, add_cmd_delimiter): helpers.logprefix = 'c : ' debug1('Starting client with Python version %s' @@ -554,6 +554,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, (serverproc, serversock) = ssh.connect( ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, + add_cmd_delimiter=add_cmd_delimiter, options=dict(latency_control=latency_control, latency_buffer_size=latency_buffer_size, auto_hosts=auto_hosts, @@ -755,7 +756,7 @@ def main(listenip_v6, listenip_v4, latency_buffer_size, dns, nslist, method_name, seed_hosts, auto_hosts, auto_nets, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, - user, group, sudo_pythonpath, tmark): + user, group, sudo_pythonpath, add_cmd_delimiter, tmark): if not remotename: raise Fatal("You must use -r/--remote to specify a remote " @@ -1103,7 +1104,7 @@ def feature_status(label, enabled, available): return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, latency_buffer_size, dns_listener, seed_hosts, auto_hosts, auto_nets, - daemon, to_nameserver) + daemon, to_nameserver, add_cmd_delimiter) finally: try: if daemon: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index 548081598..3e98eee2e 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -115,6 +115,7 @@ def main(): opt.user, opt.group, opt.sudo_pythonpath, + opt.add_cmd_delimiter, opt.tmark) if return_code == 0: diff --git a/sshuttle/options.py b/sshuttle/options.py index acd46da58..84ff26f00 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -301,6 +301,14 @@ def convert_arg_line_to_args(self, arg_line): the command to use to connect to the remote [%(default)s] """ ) +parser.add_argument( + "--no-cmd-delimiter", + action="http://github.com/store_false", + dest="add_cmd_delimiter", + help=""" + do not add a double dash before the python command + """ +) parser.add_argument( "--seed-hosts", metavar="HOSTNAME[,HOSTNAME]", diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index d7293fa03..4ce2f5636 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -84,7 +84,7 @@ def parse_hostport(rhostport): return username, password, port, host -def connect(ssh_cmd, rhostport, python, stderr, options): +def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): username, password, port, host = parse_hostport(rhostport) if username: rhost = "{}@{}".format(username, host) @@ -183,13 +183,15 @@ def connect(ssh_cmd, rhostport, python, stderr, options): if password is not None: os.environ['SSHPASS'] = str(password) argv = (["sshpass", "-e"] + sshl + - portl + - [rhost, '--', pycmd]) + portl + [rhost]) else: - argv = (sshl + - portl + - [rhost, '--', pycmd]) + argv = (sshl + portl + [rhost]) + + if add_cmd_delimiter: + argv += ['--', pycmd] + else: + argv += [pycmd] # Our which() function searches for programs in get_path() # directories (which include PATH). This step isn't strictly From fdcc840b7b93334c7f7eca6e240f2cfc81b93a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:04:25 +0000 Subject: [PATCH 162/275] Bump zipp from 3.17.0 to 3.19.1 Bumps [zipp](https://github.com/jaraco/zipp) from 3.17.0 to 3.19.1. - [Release notes](https://github.com/jaraco/zipp/releases) - [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) - [Commits](https://github.com/jaraco/zipp/compare/v3.17.0...v3.19.1) --- updated-dependencies: - dependency-name: zipp dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 31ea0501e..1c698c8a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -829,18 +829,18 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "zipp" -version = "3.17.0" +version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" From 9c3107bed7638068ef61bc729470fadfe99a3556 Mon Sep 17 00:00:00 2001 From: Antoine Jacoutot Date: Sat, 19 Nov 2022 18:46:47 +0100 Subject: [PATCH 163/275] Unbreak OpenBSD runtime. sizeof(struct pfioc_rule) changed in recent OpenBSD releases. This fixes the ioctl call to DIOCCHANGERULE. --- sshuttle/methods/pf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index d5ed06af5..2d679785c 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -266,7 +266,7 @@ class pfioc_natlook(Structure): ("proto_variant", c_uint8), ("direction", c_uint8)] - self.pfioc_rule = c_char * 3424 + self.pfioc_rule = c_char * 3408 self.pfioc_natlook = pfioc_natlook super(OpenBsd, self).__init__() From fd63611b5afe40fbab4a3ecf1eb410d7575d9c9a Mon Sep 17 00:00:00 2001 From: Antoine Jacoutot Date: Thu, 11 Jul 2024 14:08:04 +0200 Subject: [PATCH 164/275] Fix pf_rule size in OpenBSD. --- tests/client/test_methods_pf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/client/test_methods_pf.py b/tests/client/test_methods_pf.py index 46de24c74..5cd61faba 100644 --- a/tests/client/test_methods_pf.py +++ b/tests/client/test_methods_pf.py @@ -418,8 +418,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): '0x01') assert mock_ioctl.mock_calls == [ - call(mock_pf_get_dev(), 0xcd60441a, ANY), - call(mock_pf_get_dev(), 0xcd60441a, ANY), + call(mock_pf_get_dev(), 0xcd50441a, ANY), + call(mock_pf_get_dev(), 0xcd50441a, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), @@ -470,8 +470,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): None, '0x01') assert mock_ioctl.mock_calls == [ - call(mock_pf_get_dev(), 0xcd60441a, ANY), - call(mock_pf_get_dev(), 0xcd60441a, ANY), + call(mock_pf_get_dev(), 0xcd50441a, ANY), + call(mock_pf_get_dev(), 0xcd50441a, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), From f10535edf4d4a729d4188cf274a4e95e5a788d84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:45:19 +0000 Subject: [PATCH 165/275] Bump furo from 2024.5.6 to 2024.7.18 Bumps [furo](https://github.com/pradyunsg/furo) from 2024.5.6 to 2024.7.18. - [Release notes](https://github.com/pradyunsg/furo/releases) - [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md) - [Commits](https://github.com/pradyunsg/furo/compare/2024.05.06...2024.07.18) --- updated-dependencies: - dependency-name: furo dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 02f42e581..0489d9256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Sphinx==7.1.2 -furo==2024.5.6 +furo==2024.7.18 From bf2db7239306b8d4953a8679b9e4abae34ff2a36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:32:03 +0000 Subject: [PATCH 166/275] Bump pytest from 8.2.2 to 8.3.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.2 to 8.3.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.2...8.3.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 88c5561f2..e868468fb 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.2.2 +pytest==8.3.1 pytest-cov==5.0.0 flake8==7.1.0 bump2version==1.0.1 From 037ee9025e5b0f5cb80eb13a75171a5bc16edddf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:41:23 +0000 Subject: [PATCH 167/275] Bump pytest from 8.2.2 to 8.3.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.2 to 8.3.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.2...8.3.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1c698c8a3..b43a7ba40 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, + {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, ] [package.dependencies] @@ -641,7 +641,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] From bd3164db2244de3a663ffd3fdd716e4403b4f1f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:49:12 +0000 Subject: [PATCH 168/275] Bump pytest from 8.3.1 to 8.3.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.1...8.3.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index e868468fb..08c543c09 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==8.3.1 +pytest==8.3.2 pytest-cov==5.0.0 flake8==7.1.0 bump2version==1.0.1 From 834ac02a5dfa5ddc425f5cfa44e36ea45dfea333 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:19:04 +0000 Subject: [PATCH 169/275] Bump pytest from 8.3.1 to 8.3.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.1...8.3.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index b43a7ba40..4ea29c1dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -627,13 +627,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.1" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, - {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] From 2408563f3b9f41a93f4ecc5e5352686c3a985d65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 10:33:58 +0000 Subject: [PATCH 170/275] Bump flake8 from 7.1.0 to 7.1.1 Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.0 to 7.1.1. - [Commits](https://github.com/pycqa/flake8/compare/7.1.0...7.1.1) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 08c543c09..48661fb34 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==8.3.2 pytest-cov==5.0.0 -flake8==7.1.0 +flake8==7.1.1 bump2version==1.0.1 From 5a64c81b5bb6b9a84cde1226cda1e1269ac32ae7 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 171/275] experimental windows method --- docs/requirements.rst | 3 +- docs/windows.rst | 10 ++- sshuttle/__main__.py | 6 +- sshuttle/client.py | 160 +++++++++++++++++++++------------- sshuttle/firewall.py | 37 ++++++-- sshuttle/helpers.py | 11 +++ sshuttle/methods/__init__.py | 3 +- sshuttle/methods/windivert.py | 126 ++++++++++++++++++++++++++ sshuttle/options.py | 3 +- sshuttle/ssh.py | 72 +++++++++++---- sshuttle/ssnet.py | 63 ++++++++----- 11 files changed, 382 insertions(+), 112 deletions(-) create mode 100644 sshuttle/methods/windivert.py diff --git a/docs/requirements.rst b/docs/requirements.rst index 9a2e1867a..f5a0936fb 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -65,8 +65,7 @@ Requires: Windows ~~~~~~~ -Not officially supported, however can be made to work with Vagrant. Requires -cmd.exe with Administrator access. See :doc:`windows` for more information. +Experimental built-in support available. See :doc:`windows` for more information. Server side Requirements diff --git a/docs/windows.rst b/docs/windows.rst index 9103ec948..2561cd390 100644 --- a/docs/windows.rst +++ b/docs/windows.rst @@ -1,7 +1,13 @@ Microsoft Windows ================= -Currently there is no built in support for running sshuttle directly on -Microsoft Windows. + +Experimental support:: + +Experimental built-in support for Windows is availble through `windivert` method. +You have to install https://pypi.org/project/pydivert pacakge. You need Administrator privileges to use windivert method + + +Use Linux VM on Windows:: What we can really do is to create a Linux VM with Vagrant (or simply Virtualbox if you like). In the Vagrant settings, remember to turn on bridged diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index b4bd42f66..c885caa9f 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -1,4 +1,8 @@ """Coverage.py's main entry point.""" import sys +import os from sshuttle.cmdline import main -sys.exit(main()) +from sshuttle.helpers import debug3 +exit_code=main() +debug3("Exiting process %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) +sys.exit(exit_code) diff --git a/sshuttle/client.py b/sshuttle/client.py index 730046b3e..40dfd70da 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -14,7 +14,7 @@ import sshuttle.sdnotify as sdnotify from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ - resolvconf_nameservers, which + resolvconf_nameservers, which, is_admin_user from sshuttle.methods import get_method, Features from sshuttle import __version__ try: @@ -219,67 +219,97 @@ def __init__(self, method_name, sudo_pythonpath): # A list of commands that we can try to run to start the firewall. argv_tries = [] - if os.getuid() == 0: # No need to elevate privileges + if is_admin_user(): # No need to elevate privileges argv_tries.append(argvbase) else: - # Linux typically uses sudo; OpenBSD uses doas. However, some - # Linux distributions are starting to use doas. - sudo_cmd = ['sudo', '-p', '[local sudo] Password: '] - doas_cmd = ['doas'] - - # For clarity, try to replace executable name with the - # full path. - doas_path = which("doas") - if doas_path: - doas_cmd[0] = doas_path - sudo_path = which("sudo") - if sudo_path: - sudo_cmd[0] = sudo_path - - # sudo_pythonpath indicates if we should set the - # PYTHONPATH environment variable when elevating - # privileges. This can be adjusted with the - # --no-sudo-pythonpath option. - if sudo_pythonpath: - pp_prefix = ['/usr/bin/env', - 'PYTHONPATH=%s' % - os.path.dirname(os.path.dirname(__file__))] - sudo_cmd = sudo_cmd + pp_prefix - doas_cmd = doas_cmd + pp_prefix - - # Final order should be: sudo/doas command, env - # pythonpath, and then argvbase (sshuttle command). - sudo_cmd = sudo_cmd + argvbase - doas_cmd = doas_cmd + argvbase - - # If we can find doas and not sudo or if we are on - # OpenBSD, try using doas first. - if (doas_path and not sudo_path) or \ - platform.platform().startswith('OpenBSD'): - argv_tries = [doas_cmd, sudo_cmd, argvbase] + if sys.platform == 'win32': + argv_tries.append(argvbase) + # runas_path = which("runas") + # if runas_path: + # argv_tries.append(['runas' , '/noprofile', '/user:Administrator', 'python']) else: - argv_tries = [sudo_cmd, doas_cmd, argvbase] + # Linux typically uses sudo; OpenBSD uses doas. However, some + # Linux distributions are starting to use doas. + sudo_cmd = ['sudo', '-p', '[local sudo] Password: '] + doas_cmd = ['doas'] + + # For clarity, try to replace executable name with the + # full path. + doas_path = which("doas") + if doas_path: + doas_cmd[0] = doas_path + sudo_path = which("sudo") + if sudo_path: + sudo_cmd[0] = sudo_path + + # sudo_pythonpath indicates if we should set the + # PYTHONPATH environment variable when elevating + # privileges. This can be adjusted with the + # --no-sudo-pythonpath option. + if sudo_pythonpath: + pp_prefix = ['/usr/bin/env', + 'PYTHONPATH=%s' % + os.path.dirname(os.path.dirname(__file__))] + sudo_cmd = sudo_cmd + pp_prefix + doas_cmd = doas_cmd + pp_prefix + + # Final order should be: sudo/doas command, env + # pythonpath, and then argvbase (sshuttle command). + sudo_cmd = sudo_cmd + argvbase + doas_cmd = doas_cmd + argvbase + + # If we can find doas and not sudo or if we are on + # OpenBSD, try using doas first. + if (doas_path and not sudo_path) or \ + platform.platform().startswith('OpenBSD'): + argv_tries = [doas_cmd, sudo_cmd, argvbase] + else: + argv_tries = [sudo_cmd, doas_cmd, argvbase] # Try all commands in argv_tries in order. If a command # produces an error, try the next one. If command is # successful, set 'success' variable and break. success = False for argv in argv_tries: - # we can't use stdin/stdout=subprocess.PIPE here, as we - # normally would, because stupid Linux 'su' requires that - # stdin be attached to a tty. Instead, attach a - # *bidirectional* socket to its stdout, and use that for - # talking in both directions. - (s1, s2) = socket.socketpair() - def setup(): - # run in the child process - s2.close() + if sys.platform != 'win32': + # we can't use stdin/stdout=subprocess.PIPE here, as we + # normally would, because stupid Linux 'su' requires that + # stdin be attached to a tty. Instead, attach a + # *bidirectional* socket to its stdout, and use that for + # talking in both directions. + (s1, s2) = socket.socketpair() + pstdout = s1 + pstdin = s1 + penv = None + def preexec_fn(): + # run in the child process + s2.close() + def get_pfile(): + s1.close() + return s2.makefile('rwb') + + else: + (s1, s2) = socket.socketpair() + pstdout = None + pstdin = ssubprocess.PIPE + preexec_fn = None + penv = os.environ.copy() + penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) + def get_pfile(): + import base64 + socket_share_data = s1.share(self.p.pid) + s1.close() + socket_share_data_b64 = base64.b64encode(socket_share_data) + # debug3(f"{socket_share_data_b64=}") + self.p.stdin.write(socket_share_data_b64 + b'\n') + self.p.stdin.flush() + return s2.makefile('rwb') try: debug1("Starting firewall manager with command: %r" % argv) - self.p = ssubprocess.Popen(argv, stdout=s1, stdin=s1, - preexec_fn=setup) + self.p = ssubprocess.Popen(argv, stdout=pstdout, stdin=pstdin, env=penv, + preexec_fn=preexec_fn) # No env: Talking to `FirewallClient.start`, which has no i18n. except OSError as e: # This exception will occur if the program isn't @@ -287,11 +317,15 @@ def setup(): debug1('Unable to start firewall manager. Popen failed. ' 'Command=%r Exception=%s' % (argv, e)) continue - self.argv = argv - s1.close() - self.pfile = s2.makefile('rwb') - line = self.pfile.readline() + + self.pfile = get_pfile() + + try: + line = self.pfile.readline() + except ConnectionResetError: + # happens in Windows, when subprocess exists + line='' rv = self.p.poll() # Check if process is still running if rv: @@ -327,14 +361,14 @@ def setup(): 'Command=%r' % (skipped_text, self.argv)) continue - method_name = line[6:-1] + method_name = line.strip()[6:] self.method = get_method(method_name.decode("ASCII")) self.method.set_firewall(self) success = True break if not success: - raise Fatal("All attempts to elevate privileges failed.") + raise Fatal("All attempts to run firewall client with elevated privileges were failed.") def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, @@ -397,9 +431,9 @@ def start(self): (udp, user, group, bytes(self.tmark, 'ascii'), os.getpid())) self.pfile.flush() - line = self.pfile.readline() + line = self.pfile.readline().strip() self.check() - if line != b'STARTED\n': + if line != b'STARTED': raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) def sethostip(self, hostname, ip): @@ -562,24 +596,26 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, auto_nets=auto_nets)) except socket.error as e: if e.args[0] == errno.EPIPE: + debug3('Error: EPIPE: ' + repr(e)) raise Fatal("failed to establish ssh session (1)") else: raise - mux = Mux(serversock.makefile("rb"), serversock.makefile("wb")) + rfile, wfile = serversock.makefile("rb"), serversock.makefile("wb") + mux = Mux(rfile, wfile) handlers.append(mux) expected = b'SSHUTTLE0001' - try: v = 'x' while v and v != b'\0': - v = serversock.recv(1) + v = rfile.read(1) v = 'x' while v and v != b'\0': - v = serversock.recv(1) - initstring = serversock.recv(len(expected)) + v = rfile.read(1) + initstring = rfile.read(len(expected)) except socket.error as e: if e.args[0] == errno.ECONNRESET: + debug3('Error: ECONNRESET ' + repr(e)) raise Fatal("failed to establish ssh session (2)") else: raise diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 6cc1bd983..201f017a0 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -7,10 +7,12 @@ import platform import traceback import subprocess as ssubprocess +import base64 +import io import sshuttle.ssyslog as ssyslog import sshuttle.helpers as helpers -from sshuttle.helpers import log, debug1, debug2, Fatal +from sshuttle.helpers import is_admin_user, log, debug1, debug2, Fatal from sshuttle.methods import get_auto_method, get_method HOSTSFILE = '/etc/hosts' @@ -87,8 +89,8 @@ def firewall_exit(signum, frame): # Isolate function that needs to be replaced for tests -def setup_daemon(): - if os.getuid() != 0: +def _setup_daemon_unix(): + if not is_admin_user(): raise Fatal('You must be root (or enable su/sudo) to set the firewall') # don't disappear if our controlling terminal or stdout/stderr @@ -113,6 +115,25 @@ def setup_daemon(): return sys.stdin, sys.stdout +def _setup_daemon_windows(): + if not is_admin_user(): + raise Fatal('You must be administrator to set the firewall') + + signal.signal(signal.SIGTERM, firewall_exit) + signal.signal(signal.SIGINT, firewall_exit) + socket_share_data_b64 = sys.stdin.readline() + # debug3(f'FROM_SHARE ${socket_share_data_b64=}') + socket_share_data = base64.b64decode(socket_share_data_b64) + sock = socket.fromshare(socket_share_data) + sys.stdin = io.TextIOWrapper(sock.makefile('rb')) + sys.stdout = io.TextIOWrapper(sock.makefile('wb')) + return sys.stdin, sys.stdout + +if sys.platform == 'win32': + setup_daemon = _setup_daemon_windows +else: + setup_daemon = _setup_daemon_unix + # Note that we're sorting in a very particular order: # we need to go from smaller, more specific, port ranges, to larger, # less-specific, port ranges. At each level, we order by subnet @@ -190,9 +211,13 @@ def main(method_name, syslog): # we wait until we get some input before creating the rules. That way, # sshuttle can launch us as early as possible (and get sudo password # authentication as early in the startup process as possible). - line = stdin.readline(128) - if not line: - return # parent died; nothing to do + try: + line = stdin.readline(128) + if not line: + return # parent died; nothing to do + except ConnectionResetError: + # On windows, this is thrown when parent process closes it's socket pair end + return subnets = [] if line != 'ROUTES\n': diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 2d747e442..c2d03b180 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -220,3 +220,14 @@ def which(file, mode=os.F_OK | os.X_OK): else: debug2("which() could not find '%s' in %s" % (file, path)) return rv + +def is_admin_user(): + if sys.platform == 'win32': + import ctypes + # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script/41930586#41930586 + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except: + return False + + return os.getuid() != 0 diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 962529b6c..d934ab737 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -1,6 +1,7 @@ import importlib import socket import struct +import sys import errno import ipaddress from sshuttle.helpers import Fatal, debug3 @@ -109,7 +110,7 @@ def get_method(method_name): def get_auto_method(): debug3("Selecting a method automatically...") # Try these methods, in order: - methods_to_try = ["nat", "nft", "pf", "ipfw"] + methods_to_try = ["nat", "nft", "pf", "ipfw"] if sys.platform != "win32" else ["windivert"] for m in methods_to_try: method = get_method(m) if method.is_supported(): diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py new file mode 100644 index 000000000..75bca5a07 --- /dev/null +++ b/sshuttle/methods/windivert.py @@ -0,0 +1,126 @@ +import sys +import ipaddress +import threading +from collections import namedtuple + + +try: + import pydivert +except ImportError: + raise Fatal('Could not import pydivert module. windivert requires https://pypi.org/project/pydivert') + +from sshuttle.methods import BaseMethod +from sshuttle.helpers import log, debug1, debug2, Fatal + +# https://reqrypt.org/windivert-doc.html#divert_iphdr + + +ConnectionTuple = namedtuple( + "ConnectionTuple", ["protocol", "src_addr", "src_port", "dst_addr", "dst_port"] +) + +class ConnectionTracker: + def __init__(self) -> None: + self.d = {} + + def add_tcp(self, src_addr, src_port, dst_addr, dst_port): + k = ("TCP", src_addr, src_port) + v = (dst_addr, dst_port) + if self.d.get(k) != v: + debug1("Adding tcp connection to tracker:" + repr((src_addr, src_port, dst_addr, dst_port))) + self.d[k] = v + + def get_tcp(self, src_addr, src_port): + try: + return ConnectionTuple( + "TCP", src_addr, src_port, *self.d[("TCP", src_addr, src_port)] + ) + except KeyError: + return None + + +class Method(BaseMethod): + + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, + user, tmark): + log( f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + # port=12300, dnsport=0, nslist=[], family=, + # subnets=[(2, 24, False, '10.111.10.0', 0, 0), (2, 16, False, '169.254.0.0', 0, 0), (2, 24, False, '172.31.0.0', 0, 0), (2, 16, False, '192.168.0.0', 0, 0), (2, 32, True, '0.0.0.0', 0, 0)], + # udp=False, user=None, tmark='0x01' + self.conntrack = ConnectionTracker() + proxy_addr = "10.0.2.15" + + subnet_addreses = [] + for (_, mask, exclude, network_addr, fport, lport) in subnets: + if exclude: + continue + assert fport == 0, 'custom port range not supported' + assert lport == 0, 'custom port range not supported' + subnet_addreses.append("%s/%s" % (network_addr, mask)) + + debug2("subnet_addreses=%s proxy_addr=%s:%s" % (subnet_addreses,proxy_addr,port)) + + # check permission + with pydivert.WinDivert('false'): + pass + + threading.Thread(name='outbound_divert', target=self._outbound_divert, args=(subnet_addreses, proxy_addr, port), daemon=True).start() + threading.Thread(name='inbound_divert', target=self._inbound_divert, args=(proxy_addr, port), daemon=True).start() + + def restore_firewall(self, port, family, udp, user): + pass + + def get_supported_features(self): + result = super(Method, self).get_supported_features() + result.user = False + result.dns = False + result.ipv6 = False + return result + + def get_tcp_dstip(self, sock): + return ('172.31.0.141', 80) + + def is_supported(self): + if sys.platform == 'win32': + return True + return False + + + + def _outbound_divert(self, subnets, proxy_addr, proxy_port): + # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: + filter = "outbound and ip and tcp" + subnet_selectors = [] + for cidr in subnets: + ip_network = ipaddress.ip_network(cidr) + first_ip = ip_network.network_address + last_ip = ip_network.broadcast_address + subnet_selectors.append(f"(ip.DstAddr >= {first_ip} and ip.DstAddr <= {last_ip})") + filter = f"{filter} and ({'or'.join(subnet_selectors)}) " + + debug1(f"[OUTBOUND] {filter=}") + with pydivert.WinDivert(filter) as w: + for pkt in w: + # debug3(repr(pkt)) + self.conntrack.add_tcp(pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port) + pkt.ipv4.dst_addr = proxy_addr + pkt.tcp.dst_port = proxy_port + w.send(pkt, recalculate_checksum=True) + + + def _inbound_divert(self, proxy_addr, proxy_port): + filter = f"inbound and ip and tcp and ip.SrcAddr == {proxy_addr} and tcp.SrcPort == {proxy_port}" + debug2(f"[INBOUND] {filter=}") + with pydivert.WinDivert(filter) as w: + for pkt in w: + # debug2(repr(conntrack.d)) + # debug2(repr((pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port))) + conn = self.conntrack.get_tcp(pkt.dst_addr, pkt.dst_port) + if not conn: + debug2("Unexpcted packet:" + repr((pkt.protocol,pkt.src_addr,pkt.src_port,pkt.dst_addr,pkt.dst_port))) + continue + pkt.ipv4.src_addr = conn.dst_addr + pkt.tcp.src_port = conn.dst_port + w.send(pkt, recalculate_checksum=True) + + diff --git a/sshuttle/options.py b/sshuttle/options.py index 84ff26f00..93468ee5e 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -1,5 +1,6 @@ import re import socket +import sys from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal from sshuttle import __version__ @@ -236,7 +237,7 @@ def convert_arg_line_to_args(self, arg_line): parser.add_argument( "--method", - choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"], + choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"] if sys.platform != 'win32' else ["auto", "windivert"], metavar="TYPE", default="auto", help=""" diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 4ce2f5636..9ffd7c900 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -175,9 +175,10 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): # case, sshuttle might not work at all since it is not # possible to run python on the remote machine---even if # it is present. + devnull='/dev/null' pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s; exit 97") % \ - (os.devnull, quote(pyscript)) + (devnull, quote(pyscript)) pycmd = ("/bin/sh -c {}".format(quote(pycmd))) if password is not None: @@ -203,19 +204,56 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path())) argv[0] = abs_path - (s1, s2) = socket.socketpair() - - def setup(): - # runs in the child process - s2.close() - s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) - s1.close() - - debug2('executing: %r' % argv) - p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, - close_fds=True, stderr=stderr) - os.close(s1a) - os.close(s1b) - s2.sendall(content) - s2.sendall(content2) - return p, s2 + + if sys.platform != 'win32': + (s1, s2) = socket.socketpair() + def preexec_fn(): + # runs in the child process + s2.close() + pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno()) + s1.close() + + def get_serversock(): + os.close(pstdin) + os.close(pstdout) + return s2 + else: + (s1, s2) = socket.socketpair() + preexec_fn = None + pstdin = ssubprocess.PIPE + pstdout = ssubprocess.PIPE + def get_serversock(): + import threading + def steam_stdout_to_sock(): + while True: + data = p.stdout.read(1) + if not data: + debug2("EOF on ssh process stdout. Process probably exited") + break + n = s1.sendall(data) + print("<<<<< p.stdout.read()", len(data), '->', n, data[:min(32,len(data))]) + def stream_sock_to_stdin(): + while True: + data = s1.recv(16384) + if not data: + print(">>>>>> EOF stream_sock_to_stdin") + break + n = p.stdin.write(data) + print(">>>>>> s1.recv()", len(data) , "->" , n , data[:min(32,len(data))]) + p.communicate + threading.Thread(target=steam_stdout_to_sock, name='steam_stdout_to_sock', daemon=True).start() + threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() + # s2.setblocking(False) + return s2 + + # https://stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows + close_fds = False if sys.platform == 'win32' else True + + debug2("executing: %r" % argv) + p = ssubprocess.Popen(argv, stdin=pstdin, stdout=pstdout, preexec_fn=preexec_fn, + close_fds=close_fds, stderr=stderr, bufsize=0) + + serversock = get_serversock() + serversock.sendall(content) + serversock.sendall(content2) + return p, serversock diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index 19d60ef61..29f306465 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -4,7 +4,9 @@ import errno import select import os -import fcntl + +if sys.platform != "win32": + import fcntl from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal @@ -213,7 +215,10 @@ def uwrite(self, buf): return 0 # still connecting self.wsock.setblocking(False) try: - return _nb_clean(os.write, self.wsock.fileno(), buf) + if sys.platform == 'win32': + return _nb_clean(self.wsock.send, buf) + else: + return _nb_clean(os.write, self.wsock.fileno(), buf) except OSError: _, e = sys.exc_info()[:2] if e.errno == errno.EPIPE: @@ -236,7 +241,10 @@ def uread(self): return self.rsock.setblocking(False) try: - return _nb_clean(os.read, self.rsock.fileno(), 65536) + if sys.platform == 'win32': + return _nb_clean(self.rsock.recv, 65536) + else: + return _nb_clean(os.read, self.rsock.fileno(), 65536) except OSError: _, e = sys.exc_info()[:2] self.seterr('uread: %s' % e) @@ -431,15 +439,22 @@ def got_packet(self, channel, cmd, data): callback(cmd, data) def flush(self): - try: - os.set_blocking(self.wfile.fileno(), False) - except AttributeError: - # python < 3.5 - flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL) - flags |= os.O_NONBLOCK - fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) + if sys.platform != "win32": + try: + os.set_blocking(self.wfile.fileno(), False) + except AttributeError: + # python < 3.5 + flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL) + flags |= os.O_NONBLOCK + fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) + else: + self.wfile.raw._sock.setblocking(False) + if self.outbuf and self.outbuf[0]: - wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) + if sys.platform == 'win32': + wrote = _nb_clean(self.wfile.raw._sock.send, self.outbuf[0]) + else: + wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0]))) if wrote: self.outbuf[0] = self.outbuf[0][wrote:] @@ -447,18 +462,26 @@ def flush(self): self.outbuf[0:1] = [] def fill(self): - try: - os.set_blocking(self.rfile.fileno(), False) - except AttributeError: - # python < 3.5 - flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL) - flags |= os.O_NONBLOCK - fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) + if sys.platform != "win32": + try: + os.set_blocking(self.rfile.fileno(), False) + except AttributeError: + # python < 3.5 + flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL) + flags |= os.O_NONBLOCK + fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) + else: + self.rfile.raw._sock.setblocking(False) + try: # If LATENCY_BUFFER_SIZE is inappropriately large, we will # get a MemoryError here. Read no more than 1MiB. - read = _nb_clean(os.read, self.rfile.fileno(), - min(1048576, LATENCY_BUFFER_SIZE)) + if sys.platform == 'win32': + read = _nb_clean(self.rfile.raw._sock.recv, + min(1048576, LATENCY_BUFFER_SIZE)) + else: + read = _nb_clean(os.read, self.rfile.fileno(), + min(1048576, LATENCY_BUFFER_SIZE)) except OSError: _, e = sys.exc_info()[:2] raise Fatal('other end: %r' % e) From 2c744761249e732210a46c0aae3d0b463c80e84d Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 172/275] windivert - basic working connection tracker --- sshuttle/__main__.py | 2 +- sshuttle/methods/windivert.py | 195 ++++++++++++++++++++++++++++------ 2 files changed, 166 insertions(+), 31 deletions(-) diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index c885caa9f..e3663b5ed 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -5,4 +5,4 @@ from sshuttle.helpers import debug3 exit_code=main() debug3("Exiting process %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) -sys.exit(exit_code) +sys.exit(exit_code) \ No newline at end of file diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 75bca5a07..6d10f8330 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -1,8 +1,14 @@ +import os import sys import ipaddress import threading from collections import namedtuple - +import socket +from multiprocessing import shared_memory +import struct +from functools import wraps +from enum import IntEnum +import time try: import pydivert @@ -10,33 +16,147 @@ raise Fatal('Could not import pydivert module. windivert requires https://pypi.org/project/pydivert') from sshuttle.methods import BaseMethod -from sshuttle.helpers import log, debug1, debug2, Fatal +from sshuttle.helpers import debug3, log, debug1, debug2, Fatal # https://reqrypt.org/windivert-doc.html#divert_iphdr ConnectionTuple = namedtuple( - "ConnectionTuple", ["protocol", "src_addr", "src_port", "dst_addr", "dst_port"] + "ConnectionTuple", ["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", 'state'] ) -class ConnectionTracker: - def __init__(self) -> None: - self.d = {} - def add_tcp(self, src_addr, src_port, dst_addr, dst_port): - k = ("TCP", src_addr, src_port) - v = (dst_addr, dst_port) - if self.d.get(k) != v: - debug1("Adding tcp connection to tracker:" + repr((src_addr, src_port, dst_addr, dst_port))) - self.d[k] = v +MAX_CONNECTIONS = 2 #_000 + +class IPProtocol(IntEnum): + TCP = socket.IPPROTO_TCP + UDP = socket.IPPROTO_UDP + +class ConnState(IntEnum): + TCP_SYN_SEND = 10 + TCP_SYN_ACK_RECV = 11 + TCP_FIN_SEND = 20 + TCP_FIN_RECV = 21 + +def repr_pkt(p): + r = f"{p.direction.name} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" + if p.tcp: + t = p.tcp + r += f" {len(t.payload)}B (" + r += '+'.join(f.upper() for f in ('fin','syn', "rst", "psh", 'ack', 'urg', 'ece', 'cwr', 'ns') if getattr(t, f)) + r += f') SEQ#{t.seq_num}' + if t.ack: + r += f' ACK#{t.ack_num}' + r += f' WZ={t.window_size}' + else: + r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}" + return f"" + +def synchronized_method(lock): + def decorator(method): + @wraps(method) + def wrapped(self, *args, **kwargs): + with getattr(self, lock): + return method(self, *args, **kwargs) + return wrapped + return decorator + +class ConnTrack: + def __init__(self, name, max_connections=0) -> None: + self.struct_full_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H', '16s', 'H', 'L', 'B'))) + self.struct_src_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H'))) + self.struct_state_tuple = struct.Struct('>' + ''.join(('L', 'B'))) - def get_tcp(self, src_addr, src_port): try: - return ConnectionTuple( - "TCP", src_addr, src_port, *self.d[("TCP", src_addr, src_port)] - ) - except KeyError: - return None + self.max_connections = max_connections + self.shm_list = shared_memory.ShareableList([bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name) + self.is_owner = True + self.next_slot = 0 + self.used_slotes = set() + self.rlock = threading.RLock() + except FileExistsError: + self.is_owner = False + self.shm_list = shared_memory.ShareableList(name=name) + self.max_connections = len(self.shm_list) + + debug2(f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} shm_size={self.shm_list.shm.size}B") + + @synchronized_method('rlock') + def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + if len(self.used_slotes) >= self.max_connections: + raise RuntimeError(f"No slot avaialble in ConnTrack {len(self.used_slotes)}/{self.max_connections}") + + if self.get(proto, src_addr, src_port): + return + + for _ in range(self.max_connections): + if self.next_slot not in self.used_slotes: + break + self.next_slot = (self.next_slot +1) % self.max_connections + else: + raise RuntimeError("No slot avaialble in ConnTrack") # should not be here + + src_addr = ipaddress.ip_address(src_addr) + dst_addr = ipaddress.ip_address(dst_addr) + assert src_addr.version == dst_addr.version + ip_version = src_addr.version + state_epoch = int(time.time()) + entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state) + packed = self.struct_full_tuple.pack(*entry) + self.shm_list[self.next_slot] = packed + self.used_slotes.add(self.next_slot) + proto = IPProtocol(proto) + debug3(f"ConnTrack: added connection ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slotes)}") + + @synchronized_method('rlock') + def update(self, proto, src_addr, src_port, state): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for i in self.used_slotes: + if self.shm_list[i].startswith(packed): + state_epoch = int(time.time()) + self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) + debug3(f"ConnTrack: updated connection ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slotes)}") + return self._unpack(self.shm_list[i]) + else: + debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slotes)}") + + @synchronized_method('rlock') + def remove(self, proto, src_addr, src_port): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for i in self.used_slotes: + if self.shm_list[i].startswith(packed): + conn = self._unpack(self.shm_list[i]) + self.shm_list[i] = b'' + self.used_slotes.remove(i) + debug3(f"ConnTrack: removed connection ({proto.name} src={src_addr}:{src_port}) from slot={i} | #ActiveConn={len(self.used_slotes)}") + return conn + else: + debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slotes)}") + + + def get(self, proto, src_addr, src_port): + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for entry in self.shm_list: + if entry and entry.startswith(packed): + return self._unpack(entry) + + def _unpack(self, packed): + (proto, ip_version, src_addr_packed, src_port, dst_addr_packed, dst_port, state_epoch, state) = self.struct_full_tuple.unpack(packed) + dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) + src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) + return ConnectionTuple(IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state)) + + def __repr__(self): + return f"" class Method(BaseMethod): @@ -44,10 +164,7 @@ class Method(BaseMethod): def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): log( f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") - # port=12300, dnsport=0, nslist=[], family=, - # subnets=[(2, 24, False, '10.111.10.0', 0, 0), (2, 16, False, '169.254.0.0', 0, 0), (2, 24, False, '172.31.0.0', 0, 0), (2, 16, False, '192.168.0.0', 0, 0), (2, 32, True, '0.0.0.0', 0, 0)], - # udp=False, user=None, tmark='0x01' - self.conntrack = ConnectionTracker() + self.conntrack = ConnTrack(f'sshttle-windiver-{os.getppid()}', MAX_CONNECTIONS) proxy_addr = "10.0.2.15" subnet_addreses = [] @@ -58,7 +175,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, assert lport == 0, 'custom port range not supported' subnet_addreses.append("%s/%s" % (network_addr, mask)) - debug2("subnet_addreses=%s proxy_addr=%s:%s" % (subnet_addreses,proxy_addr,port)) + debug2("setup_firewall() subnet_addreses=%s proxy_addr=%s:%s" % (subnet_addreses,proxy_addr,port)) # check permission with pydivert.WinDivert('false'): @@ -78,7 +195,14 @@ def get_supported_features(self): return result def get_tcp_dstip(self, sock): - return ('172.31.0.141', 80) + if not hasattr(self, 'conntrack'): + self.conntrack = ConnTrack(f'sshttle-windiver-{os.getpid()}') + + src_addr , src_port = sock.getpeername() + c = self.conntrack.get(IPProtocol.TCP , src_addr, src_port) + if not c: + return (src_addr , src_port) + return (c.dst_addr, c.dst_port) def is_supported(self): if sys.platform == 'win32': @@ -87,6 +211,8 @@ def is_supported(self): + + def _outbound_divert(self, subnets, proxy_addr, proxy_port): # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: filter = "outbound and ip and tcp" @@ -101,8 +227,13 @@ def _outbound_divert(self, subnets, proxy_addr, proxy_port): debug1(f"[OUTBOUND] {filter=}") with pydivert.WinDivert(filter) as w: for pkt in w: - # debug3(repr(pkt)) - self.conntrack.add_tcp(pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port) + debug3(">>> " + repr_pkt(pkt)) + if pkt.tcp.syn and not pkt.tcp.ack: # SYN (start of 3-way handshake connection establishment) + self.conntrack.add(socket.IPPROTO_TCP, pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_SEND) + if pkt.tcp.fin: # FIN (start of graceful close) + self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_SEND) + if pkt.tcp.rst : # RST + self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) pkt.ipv4.dst_addr = proxy_addr pkt.tcp.dst_port = proxy_port w.send(pkt, recalculate_checksum=True) @@ -113,11 +244,15 @@ def _inbound_divert(self, proxy_addr, proxy_port): debug2(f"[INBOUND] {filter=}") with pydivert.WinDivert(filter) as w: for pkt in w: - # debug2(repr(conntrack.d)) - # debug2(repr((pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port))) - conn = self.conntrack.get_tcp(pkt.dst_addr, pkt.dst_port) + debug3("<<< " + repr_pkt(pkt)) + if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK Conenction established + conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_ACK_RECV) + elif pkt.tcp.rst or (pkt.tcp.fin and pkt.tcp.ack): # RST or FIN+ACK Connection teardown + conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) + else: + conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: - debug2("Unexpcted packet:" + repr((pkt.protocol,pkt.src_addr,pkt.src_port,pkt.dst_addr,pkt.dst_port))) + debug2("Unexpcted packet: " + repr_pkt(pkt)) continue pkt.ipv4.src_addr = conn.dst_addr pkt.tcp.src_port = conn.dst_port From bd2f9607438f65fd57e750780188e00c5cc9ec5b Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 173/275] more improvements windows support --- sshuttle/client.py | 4 +-- sshuttle/firewall.py | 40 +++++++++++++++++------------ sshuttle/methods/windivert.py | 48 +++++++++++++++++------------------ sshuttle/ssh.py | 43 ++++++++++++++++++------------- 4 files changed, 75 insertions(+), 60 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 40dfd70da..d5668d4fd 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -323,8 +323,8 @@ def get_pfile(): try: line = self.pfile.readline() - except ConnectionResetError: - # happens in Windows, when subprocess exists + except IOError: + # happens when firewall subprocess exists line='' rv = self.p.poll() # Check if process is still running diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 201f017a0..02bed9418 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -12,7 +12,7 @@ import sshuttle.ssyslog as ssyslog import sshuttle.helpers as helpers -from sshuttle.helpers import is_admin_user, log, debug1, debug2, Fatal +from sshuttle.helpers import is_admin_user, log, debug1, debug2, debug3, Fatal from sshuttle.methods import get_auto_method, get_method HOSTSFILE = '/etc/hosts' @@ -214,9 +214,11 @@ def main(method_name, syslog): try: line = stdin.readline(128) if not line: - return # parent died; nothing to do - except ConnectionResetError: - # On windows, this is thrown when parent process closes it's socket pair end + # parent probably exited + return + except IOError: + # On windows, this ConnectionResetError is thrown when parent process closes it's socket pair end + debug3('read from stdin failed: %s' % (e,)) return subnets = [] @@ -322,21 +324,26 @@ def main(method_name, syslog): socket.AF_INET, subnets_v4, udp, user, group, tmark) - flush_systemd_dns_cache() - stdout.write('STARTED\n') + if sys.platform != 'win32': + flush_systemd_dns_cache() + try: + stdout.write('STARTED\n') stdout.flush() - except IOError: - # the parent process died for some reason; he's surely been loud - # enough, so no reason to report another error + except IOError as e: + debug3('write to stdout failed: %s' % (e,)) return # Now we wait until EOF or any other kind of exception. We need # to stay running so that we don't need a *second* password # authentication at shutdown time - that cleanup is important! while 1: - line = stdin.readline(128) + try: + line = stdin.readline(128) + except IOError as e: + debug3('read from stdin failed: %s' % (e,)) + return if line.startswith('HOST '): (name, ip) = line[5:].strip().split(',', 1) hostmap[name] = ip @@ -385,11 +392,12 @@ def main(method_name, syslog): except Exception: debug2('An error occurred, ignoring it.') - try: - flush_systemd_dns_cache() - except Exception: + if sys.platform != 'win32': try: - debug1("Error trying to flush systemd dns cache.") - debug1(traceback.format_exc()) + flush_systemd_dns_cache() except Exception: - debug2("An error occurred, ignoring it.") + try: + debug1("Error trying to flush systemd dns cache.") + debug1(traceback.format_exc()) + except Exception: + debug2("An error occurred, ignoring it.") diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 6d10f8330..86b7d3473 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -26,7 +26,7 @@ ) -MAX_CONNECTIONS = 2 #_000 +WINDIVERT_MAX_CONNECTIONS = 10_000 class IPProtocol(IntEnum): TCP = socket.IPPROTO_TCP @@ -72,7 +72,7 @@ def __init__(self, name, max_connections=0) -> None: self.shm_list = shared_memory.ShareableList([bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name) self.is_owner = True self.next_slot = 0 - self.used_slotes = set() + self.used_slots = set() self.rlock = threading.RLock() except FileExistsError: self.is_owner = False @@ -85,18 +85,18 @@ def __init__(self, name, max_connections=0) -> None: def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") - if len(self.used_slotes) >= self.max_connections: - raise RuntimeError(f"No slot avaialble in ConnTrack {len(self.used_slotes)}/{self.max_connections}") + if len(self.used_slots) >= self.max_connections: + raise RuntimeError(f"No slot available in ConnTrack {len(self.used_slots)}/{self.max_connections}") if self.get(proto, src_addr, src_port): return for _ in range(self.max_connections): - if self.next_slot not in self.used_slotes: + if self.next_slot not in self.used_slots: break self.next_slot = (self.next_slot +1) % self.max_connections else: - raise RuntimeError("No slot avaialble in ConnTrack") # should not be here + raise RuntimeError("No slot available in ConnTrack") # should not be here src_addr = ipaddress.ip_address(src_addr) dst_addr = ipaddress.ip_address(dst_addr) @@ -106,9 +106,9 @@ def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state) packed = self.struct_full_tuple.pack(*entry) self.shm_list[self.next_slot] = packed - self.used_slotes.add(self.next_slot) + self.used_slots.add(self.next_slot) proto = IPProtocol(proto) - debug3(f"ConnTrack: added connection ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slotes)}") + debug3(f"ConnTrack: added connection ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slots)}") @synchronized_method('rlock') def update(self, proto, src_addr, src_port, state): @@ -116,14 +116,14 @@ def update(self, proto, src_addr, src_port, state): raise RuntimeError("Only owner can mutate ConnTrack") src_addr = ipaddress.ip_address(src_addr) packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) - for i in self.used_slotes: + for i in self.used_slots: if self.shm_list[i].startswith(packed): state_epoch = int(time.time()) self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) - debug3(f"ConnTrack: updated connection ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slotes)}") + debug3(f"ConnTrack: updated connection ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") return self._unpack(self.shm_list[i]) else: - debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slotes)}") + debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slots)}") @synchronized_method('rlock') def remove(self, proto, src_addr, src_port): @@ -131,15 +131,15 @@ def remove(self, proto, src_addr, src_port): raise RuntimeError("Only owner can mutate ConnTrack") src_addr = ipaddress.ip_address(src_addr) packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) - for i in self.used_slotes: + for i in self.used_slots: if self.shm_list[i].startswith(packed): conn = self._unpack(self.shm_list[i]) self.shm_list[i] = b'' - self.used_slotes.remove(i) - debug3(f"ConnTrack: removed connection ({proto.name} src={src_addr}:{src_port}) from slot={i} | #ActiveConn={len(self.used_slotes)}") + self.used_slots.remove(i) + debug3(f"ConnTrack: removed connection ({proto.name} src={src_addr}:{src_port}) from slot={i} | #ActiveConn={len(self.used_slots)}") return conn else: - debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slotes)}") + debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slots)}") def get(self, proto, src_addr, src_port): @@ -156,7 +156,7 @@ def _unpack(self, packed): return ConnectionTuple(IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state)) def __repr__(self): - return f"" + return f"" class Method(BaseMethod): @@ -164,24 +164,24 @@ class Method(BaseMethod): def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): log( f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") - self.conntrack = ConnTrack(f'sshttle-windiver-{os.getppid()}', MAX_CONNECTIONS) + self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getppid()}', WINDIVERT_MAX_CONNECTIONS) proxy_addr = "10.0.2.15" - subnet_addreses = [] + subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: if exclude: continue assert fport == 0, 'custom port range not supported' assert lport == 0, 'custom port range not supported' - subnet_addreses.append("%s/%s" % (network_addr, mask)) + subnet_addresses.append("%s/%s" % (network_addr, mask)) - debug2("setup_firewall() subnet_addreses=%s proxy_addr=%s:%s" % (subnet_addreses,proxy_addr,port)) + debug2("setup_firewall() subnet_addresses=%s proxy_addr=%s:%s" % (subnet_addresses,proxy_addr,port)) # check permission with pydivert.WinDivert('false'): pass - threading.Thread(name='outbound_divert', target=self._outbound_divert, args=(subnet_addreses, proxy_addr, port), daemon=True).start() + threading.Thread(name='outbound_divert', target=self._outbound_divert, args=(subnet_addresses, proxy_addr, port), daemon=True).start() threading.Thread(name='inbound_divert', target=self._inbound_divert, args=(proxy_addr, port), daemon=True).start() def restore_firewall(self, port, family, udp, user): @@ -196,7 +196,7 @@ def get_supported_features(self): def get_tcp_dstip(self, sock): if not hasattr(self, 'conntrack'): - self.conntrack = ConnTrack(f'sshttle-windiver-{os.getpid()}') + self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getpid()}') src_addr , src_port = sock.getpeername() c = self.conntrack.get(IPProtocol.TCP , src_addr, src_port) @@ -245,14 +245,14 @@ def _inbound_divert(self, proxy_addr, proxy_port): with pydivert.WinDivert(filter) as w: for pkt in w: debug3("<<< " + repr_pkt(pkt)) - if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK Conenction established + if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK connection established conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_ACK_RECV) elif pkt.tcp.rst or (pkt.tcp.fin and pkt.tcp.ack): # RST or FIN+ACK Connection teardown conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) else: conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: - debug2("Unexpcted packet: " + repr_pkt(pkt)) + debug2("Unexpected packet: " + repr_pkt(pkt)) continue pkt.ipv4.src_addr = conn.dst_addr pkt.tcp.src_port = conn.dst_port diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 9ffd7c900..6b947893a 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -12,7 +12,7 @@ from urllib.parse import urlparse import sshuttle.helpers as helpers -from sshuttle.helpers import debug2, which, get_path, Fatal +from sshuttle.helpers import debug2, debug3, which, get_path, Fatal def get_module_source(name): @@ -224,24 +224,31 @@ def get_serversock(): pstdout = ssubprocess.PIPE def get_serversock(): import threading - def steam_stdout_to_sock(): - while True: - data = p.stdout.read(1) - if not data: - debug2("EOF on ssh process stdout. Process probably exited") - break - n = s1.sendall(data) - print("<<<<< p.stdout.read()", len(data), '->', n, data[:min(32,len(data))]) + + def stream_stdout_to_sock(): + try: + fd = p.stdout.fileno() + for data in iter(lambda:os.read(fd, 16384), b''): + s1.sendall(data) + debug3(f"<<<<< p.stdout.read() {len(data)} {data[:min(32,len(data))]}...") + finally: + debug2("Thread 'stream_stdout_to_sock' exiting") + s1.close() + p.terminate() + def stream_sock_to_stdin(): - while True: - data = s1.recv(16384) - if not data: - print(">>>>>> EOF stream_sock_to_stdin") - break - n = p.stdin.write(data) - print(">>>>>> s1.recv()", len(data) , "->" , n , data[:min(32,len(data))]) - p.communicate - threading.Thread(target=steam_stdout_to_sock, name='steam_stdout_to_sock', daemon=True).start() + try: + for data in iter(lambda:s1.recv(16384), b''): + debug3(f">>>>> p.stdout.write() {len(data)} {data[:min(32,len(data))]}...") + while data: + n = p.stdin.write(data) + data = data[n:] + finally: + debug2("Thread 'stream_sock_to_stdin' exiting") + s1.close() + p.terminate() + + threading.Thread(target=stream_stdout_to_sock, name='stream_stdout_to_sock', daemon=True).start() threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() # s2.setblocking(False) return s2 From 338486930f733d2409d3c8b3c15d41da311c7fcf Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 174/275] windivert: add ipv6 support and better thread handling --- sshuttle/firewall.py | 13 ++- sshuttle/helpers.py | 5 +- sshuttle/methods/__init__.py | 3 + sshuttle/methods/windivert.py | 151 ++++++++++++++++++++++++++++------ 4 files changed, 143 insertions(+), 29 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 02bed9418..2ba3b9569 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -125,8 +125,9 @@ def _setup_daemon_windows(): # debug3(f'FROM_SHARE ${socket_share_data_b64=}') socket_share_data = base64.b64decode(socket_share_data_b64) sock = socket.fromshare(socket_share_data) - sys.stdin = io.TextIOWrapper(sock.makefile('rb')) - sys.stdout = io.TextIOWrapper(sock.makefile('wb')) + sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) + sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) + sock.close() return sys.stdin, sys.stdout if sys.platform == 'win32': @@ -324,10 +325,14 @@ def main(method_name, syslog): socket.AF_INET, subnets_v4, udp, user, group, tmark) + try: + method.wait_for_firewall_ready() + except NotImplementedError: + pass + if sys.platform != 'win32': flush_systemd_dns_cache() - try: stdout.write('STARTED\n') stdout.flush() @@ -340,7 +345,9 @@ def main(method_name, syslog): # authentication at shutdown time - that cleanup is important! while 1: try: + debug3("===================================================") line = stdin.readline(128) + debug3("===================================================" + str(line)) except IOError as e: debug3('read from stdin failed: %s' % (e,)) return diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index c2d03b180..dc78f0b27 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -14,7 +14,10 @@ def b(s): def log(s): global logprefix try: - sys.stdout.flush() + try: + sys.stdout.flush() + except (IOError,ValueError): + pass # Put newline at end of string if line doesn't have one. if not s.endswith("\n"): s = s+"\n" diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index d934ab737..bb52edd2e 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -97,6 +97,9 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def restore_firewall(self, port, family, udp, user, group): raise NotImplementedError() + def wait_for_firewall_ready(self): + raise NotImplementedError() + @staticmethod def firewall_command(line): return False diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 86b7d3473..3ed0c577f 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -9,6 +9,7 @@ from functools import wraps from enum import IntEnum import time +import traceback try: import pydivert @@ -32,6 +33,27 @@ class IPProtocol(IntEnum): TCP = socket.IPPROTO_TCP UDP = socket.IPPROTO_UDP + @property + def filter(self): + return 'tcp' if self == IPProtocol.TCP else 'udp' + +class IPFamily(IntEnum): + IPv4 = socket.AF_INET + IPv6 = socket.AF_INET6 + + @property + def filter(self): + return 'ip' if self == socket.AF_INET else 'ipv6' + + @property + def version(self): + return 4 if self == socket.AF_INET else 6 + + @property + def loopback_addr(self): + return '127.0.0.1' if self == socket.AF_INET else '::1' + + class ConnState(IntEnum): TCP_SYN_SEND = 10 TCP_SYN_ACK_RECV = 11 @@ -62,6 +84,14 @@ def wrapped(self, *args, **kwargs): return decorator class ConnTrack: + + _instance =None + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = object.__new__(cls) + return cls._instance + raise RuntimeError("ConnTrack can not be instantiated multiple times") + def __init__(self, name, max_connections=0) -> None: self.struct_full_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H', '16s', 'H', 'L', 'B'))) self.struct_src_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H'))) @@ -161,11 +191,34 @@ def __repr__(self): class Method(BaseMethod): + network_config = {} + proxy_port = None + proxy_addr = { IPFamily.IPv4: None, IPFamily.IPv6: None } + + def __init__(self, name): + super().__init__(name) + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): log( f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") - self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getppid()}', WINDIVERT_MAX_CONNECTIONS) - proxy_addr = "10.0.2.15" + + if nslist or user or udp: + raise NotImplementedError() + + family = IPFamily(family) + + # using loopback proxy address never worked. See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) + # As a workaround we use another interface ip instead. + # self.proxy_addr[family] = family.loopback_addr + for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): + if addr.is_loopback or addr.version != family.version: + continue + self.proxy_addr[family] = str(addr) + break + else: + raise Fatal(f"Could not find a non loopback proxy address for {family.name}") + + self.proxy_port = port subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: @@ -175,15 +228,33 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, assert lport == 0, 'custom port range not supported' subnet_addresses.append("%s/%s" % (network_addr, mask)) - debug2("setup_firewall() subnet_addresses=%s proxy_addr=%s:%s" % (subnet_addresses,proxy_addr,port)) + self.network_config[family] = { + 'subnets': subnet_addresses, + "nslist": nslist, + } - # check permission - with pydivert.WinDivert('false'): - pass - threading.Thread(name='outbound_divert', target=self._outbound_divert, args=(subnet_addresses, proxy_addr, port), daemon=True).start() - threading.Thread(name='inbound_divert', target=self._inbound_divert, args=(proxy_addr, port), daemon=True).start() + def wait_for_firewall_ready(self): + debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") + self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getppid()}', WINDIVERT_MAX_CONNECTIONS) + methods = (self._egress_divert, self._ingress_divert) + ready_events = [] + for fn in methods: + ev = threading.Event() + ready_events.append(ev) + def _target(): + try: + fn(ev.set) + except: + debug2(f'thread {fn.__name__} exiting due to: ' + traceback.format_exc()) + sys.stdin.close() # this will exist main thread + sys.stdout.close() + threading.Thread(name=fn.__name__, target=_target, daemon=True).start() + for ev in ready_events: + if not ev.wait(5): # at most 5 sec + raise Fatal(f"timeout in wait_for_firewall_ready()") + def restore_firewall(self, port, family, udp, user): pass @@ -209,23 +280,29 @@ def is_supported(self): return True return False + def _egress_divert(self, ready_cb): + proto = IPProtocol.TCP + filter = f"outbound and {proto.filter}" - - - - def _outbound_divert(self, subnets, proxy_addr, proxy_port): # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: - filter = "outbound and ip and tcp" - subnet_selectors = [] - for cidr in subnets: - ip_network = ipaddress.ip_network(cidr) - first_ip = ip_network.network_address - last_ip = ip_network.broadcast_address - subnet_selectors.append(f"(ip.DstAddr >= {first_ip} and ip.DstAddr <= {last_ip})") - filter = f"{filter} and ({'or'.join(subnet_selectors)}) " + family_filters = [] + for af, c in self.network_config.items(): + subnet_filters = [] + for cidr in c['subnets']: + ip_network = ipaddress.ip_network(cidr) + first_ip = ip_network.network_address + last_ip = ip_network.broadcast_address + subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") + family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") + + filter = f"{filter} and ({' or '.join(family_filters)})" debug1(f"[OUTBOUND] {filter=}") with pydivert.WinDivert(filter) as w: + ready_cb() + proxy_port = self.proxy_port + proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] + proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] for pkt in w: debug3(">>> " + repr_pkt(pkt)) if pkt.tcp.syn and not pkt.tcp.ack: # SYN (start of 3-way handshake connection establishment) @@ -234,15 +311,39 @@ def _outbound_divert(self, subnets, proxy_addr, proxy_port): self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_SEND) if pkt.tcp.rst : # RST self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) - pkt.ipv4.dst_addr = proxy_addr + + # DNAT + if pkt.ipv4 and proxy_addr_ipv4: + pkt.dst_addr = proxy_addr_ipv4 + if pkt.ipv6 and proxy_addr_ipv6: + pkt.dst_addr = proxy_addr_ipv6 pkt.tcp.dst_port = proxy_port + + # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well by setting src_addr to loopback address. + # Otherwise injecting packet will be ignored by Windows network stack as teh packet has to cross public to private address space. + # See: https://github.com/basil00/Divert/issues/82 + # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. + # >>> pkt.dst_addr = proxy_addr_ipv4 + w.send(pkt, recalculate_checksum=True) - def _inbound_divert(self, proxy_addr, proxy_port): - filter = f"inbound and ip and tcp and ip.SrcAddr == {proxy_addr} and tcp.SrcPort == {proxy_port}" - debug2(f"[INBOUND] {filter=}") + def _ingress_divert(self, ready_cb): + proto = IPProtocol.TCP + direction = 'inbound' # only when proxy address is not loopback address (Useful for testing) + ip_filters = [] + for addr in (ipaddress.ip_address(a) for a in self.proxy_addr.values() if a): + if addr.is_loopback: # Windivert treats all loopback traffic as outbound + direction = "outbound" + if addr.version == 4: + ip_filters.append(f"ip.SrcAddr=={addr}") + else: + # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this + ip_filters.append(f"ipv6.SrcAddr=={addr}") + filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" + debug2(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter) as w: + ready_cb() for pkt in w: debug3("<<< " + repr_pkt(pkt)) if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK connection established @@ -254,7 +355,7 @@ def _inbound_divert(self, proxy_addr, proxy_port): if not conn: debug2("Unexpected packet: " + repr_pkt(pkt)) continue - pkt.ipv4.src_addr = conn.dst_addr + pkt.src_addr = conn.dst_addr pkt.tcp.src_port = conn.dst_port w.send(pkt, recalculate_checksum=True) From c01794f2322f3eaf7ab49663f0688a6e240c76a9 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 175/275] windivert: garbage collect timed put connections from tracker --- sshuttle/methods/windivert.py | 77 ++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 3ed0c577f..4c3cc712d 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -55,10 +55,15 @@ def loopback_addr(self): class ConnState(IntEnum): - TCP_SYN_SEND = 10 - TCP_SYN_ACK_RECV = 11 - TCP_FIN_SEND = 20 - TCP_FIN_RECV = 21 + TCP_SYN_SENT = 11 # SYN sent + TCP_ESTABLISHED = 12 # SYN+ACK received + TCP_FIN_WAIT_1 = 91 # FIN sent + TCP_CLOSE_WAIT = 92 # FIN received + + @staticmethod + def can_timeout(state): + return state in (ConnState.TCP_SYN_SENT, ConnState.TCP_FIN_WAIT_1, ConnState.TCP_CLOSE_WAIT) + def repr_pkt(p): r = f"{p.direction.name} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" @@ -138,7 +143,7 @@ def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): self.shm_list[self.next_slot] = packed self.used_slots.add(self.next_slot) proto = IPProtocol(proto) - debug3(f"ConnTrack: added connection ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slots)}") @synchronized_method('rlock') def update(self, proto, src_addr, src_port, state): @@ -150,10 +155,10 @@ def update(self, proto, src_addr, src_port, state): if self.shm_list[i].startswith(packed): state_epoch = int(time.time()) self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) - debug3(f"ConnTrack: updated connection ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") return self._unpack(self.shm_list[i]) else: - debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slots)}") @synchronized_method('rlock') def remove(self, proto, src_addr, src_port): @@ -166,10 +171,10 @@ def remove(self, proto, src_addr, src_port): conn = self._unpack(self.shm_list[i]) self.shm_list[i] = b'' self.used_slots.remove(i) - debug3(f"ConnTrack: removed connection ({proto.name} src={src_addr}:{src_port}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") return conn else: - debug3(f"ConnTrack: connection ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slots)}") def get(self, proto, src_addr, src_port): @@ -179,12 +184,35 @@ def get(self, proto, src_addr, src_port): if entry and entry.startswith(packed): return self._unpack(entry) + @synchronized_method('rlock') + def gc(self, connection_timeout_sec=15): + now = int(time.time()) + n = 0 + for i in tuple(self.used_slots): + state_packed = self.shm_list[i][-5:] + (state_epoch, state) = self.struct_state_tuple.unpack(state_packed) + if (now - state_epoch) < connection_timeout_sec: + continue + if ConnState.can_timeout(state): + conn = self._unpack(self.shm_list[i]) + self.shm_list[i] = b'' + self.used_slots.remove(i) + n += 1 + debug3(f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}") + def _unpack(self, packed): (proto, ip_version, src_addr_packed, src_port, dst_addr_packed, dst_port, state_epoch, state) = self.struct_full_tuple.unpack(packed) dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) return ConnectionTuple(IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state)) - + + def __iter__(self): + def conn_iter(): + for i in self.used_slots: + yield self._unpack(self.shm_list[i]) + return conn_iter() + def __repr__(self): return f"" @@ -238,7 +266,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def wait_for_firewall_ready(self): debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getppid()}', WINDIVERT_MAX_CONNECTIONS) - methods = (self._egress_divert, self._ingress_divert) + methods = (self._egress_divert, self._ingress_divert, self._connection_gc) ready_events = [] for fn in methods: ev = threading.Event() @@ -305,11 +333,11 @@ def _egress_divert(self, ready_cb): proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] for pkt in w: debug3(">>> " + repr_pkt(pkt)) - if pkt.tcp.syn and not pkt.tcp.ack: # SYN (start of 3-way handshake connection establishment) - self.conntrack.add(socket.IPPROTO_TCP, pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_SEND) - if pkt.tcp.fin: # FIN (start of graceful close) - self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_SEND) - if pkt.tcp.rst : # RST + if pkt.tcp.syn and not pkt.tcp.ack: # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) + self.conntrack.add(socket.IPPROTO_TCP, pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_SENT) + if pkt.tcp.fin: # FIN sent (start of graceful close our side, and we wait for ACK) + self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1) + if pkt.tcp.rst : # RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply) self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) # DNAT @@ -346,10 +374,15 @@ def _ingress_divert(self, ready_cb): ready_cb() for pkt in w: debug3("<<< " + repr_pkt(pkt)) - if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK connection established - conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_ACK_RECV) - elif pkt.tcp.rst or (pkt.tcp.fin and pkt.tcp.ack): # RST or FIN+ACK Connection teardown + if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK received (connection established) + conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) + elif pkt.tcp.rst: # RST received - Abrupt connection teardown initiated by other side. We don't expect anymore packets + conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) + # https://wiki.wireshark.org/TCP-4-times-close.md + elif pkt.tcp.fin and pkt.tcp.ack: # FIN+ACK received (Passive close by other side. We don't expect any more packets. Other side expects an ACK) conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) + elif pkt.tcp.fin: # FIN received (Other side initiated graceful close. We expects a final ACK for a FIN packet) + conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT) else: conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: @@ -359,4 +392,8 @@ def _ingress_divert(self, ready_cb): pkt.tcp.src_port = conn.dst_port w.send(pkt, recalculate_checksum=True) - + def _connection_gc(self, ready_cb): + ready_cb() + while True: + time.sleep(5) + self.conntrack.gc() From b09cc4595be645cd104f4225a0cca7f18c300159 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 176/275] add pydivert as windows specific dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ebc65d16e..673eea80e 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ }, python_requires='>=3.8', install_requires=[ + "pydivert; os_name=='nt'" ], tests_require=[ 'pytest', From 7da3b024dd5531f29f458da688faaf4a8c4aa024 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 177/275] fix is_admin_user() helper --- sshuttle/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index dc78f0b27..b0f70a978 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -233,4 +233,4 @@ def is_admin_user(): except: return False - return os.getuid() != 0 + return os.getuid() == 0 From 482e0cbd00e76d74356aabf3cde929759f595d7c Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 178/275] pass flake8 linting --- sshuttle/__main__.py | 4 +- sshuttle/client.py | 17 +-- sshuttle/firewall.py | 6 +- sshuttle/helpers.py | 5 +- sshuttle/methods/windivert.py | 231 +++++++++++++++++++++------------- sshuttle/ssh.py | 13 +- sshuttle/ssnet.py | 6 +- 7 files changed, 172 insertions(+), 110 deletions(-) diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index e3663b5ed..327956b66 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -3,6 +3,6 @@ import os from sshuttle.cmdline import main from sshuttle.helpers import debug3 -exit_code=main() +exit_code = main() debug3("Exiting process %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) -sys.exit(exit_code) \ No newline at end of file +sys.exit(exit_code) diff --git a/sshuttle/client.py b/sshuttle/client.py index d5668d4fd..a1578dfe7 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -226,7 +226,7 @@ def __init__(self, method_name, sudo_pythonpath): argv_tries.append(argvbase) # runas_path = which("runas") # if runas_path: - # argv_tries.append(['runas' , '/noprofile', '/user:Administrator', 'python']) + # argv_tries.append(['runas' , '/noprofile', '/user:Administrator', 'python']) else: # Linux typically uses sudo; OpenBSD uses doas. However, some # Linux distributions are starting to use doas. @@ -248,8 +248,8 @@ def __init__(self, method_name, sudo_pythonpath): # --no-sudo-pythonpath option. if sudo_pythonpath: pp_prefix = ['/usr/bin/env', - 'PYTHONPATH=%s' % - os.path.dirname(os.path.dirname(__file__))] + 'PYTHONPATH=%s' % + os.path.dirname(os.path.dirname(__file__))] sudo_cmd = sudo_cmd + pp_prefix doas_cmd = doas_cmd + pp_prefix @@ -260,8 +260,7 @@ def __init__(self, method_name, sudo_pythonpath): # If we can find doas and not sudo or if we are on # OpenBSD, try using doas first. - if (doas_path and not sudo_path) or \ - platform.platform().startswith('OpenBSD'): + if (doas_path and not sudo_path) or platform.platform().startswith('OpenBSD'): argv_tries = [doas_cmd, sudo_cmd, argvbase] else: argv_tries = [sudo_cmd, doas_cmd, argvbase] @@ -282,9 +281,11 @@ def __init__(self, method_name, sudo_pythonpath): pstdout = s1 pstdin = s1 penv = None + def preexec_fn(): # run in the child process s2.close() + def get_pfile(): s1.close() return s2.makefile('rwb') @@ -295,7 +296,8 @@ def get_pfile(): pstdin = ssubprocess.PIPE preexec_fn = None penv = os.environ.copy() - penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) + penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) + def get_pfile(): import base64 socket_share_data = s1.share(self.p.pid) @@ -318,14 +320,13 @@ def get_pfile(): 'Command=%r Exception=%s' % (argv, e)) continue self.argv = argv - self.pfile = get_pfile() try: line = self.pfile.readline() except IOError: # happens when firewall subprocess exists - line='' + line = '' rv = self.p.poll() # Check if process is still running if rv: diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 2ba3b9569..5193b15dc 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -130,11 +130,13 @@ def _setup_daemon_windows(): sock.close() return sys.stdin, sys.stdout + if sys.platform == 'win32': setup_daemon = _setup_daemon_windows else: setup_daemon = _setup_daemon_unix + # Note that we're sorting in a very particular order: # we need to go from smaller, more specific, port ranges, to larger, # less-specific, port ranges. At each level, we order by subnet @@ -216,8 +218,8 @@ def main(method_name, syslog): line = stdin.readline(128) if not line: # parent probably exited - return - except IOError: + return + except IOError as e: # On windows, this ConnectionResetError is thrown when parent process closes it's socket pair end debug3('read from stdin failed: %s' % (e,)) return diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index b0f70a978..4dca60eb4 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -16,7 +16,7 @@ def log(s): try: try: sys.stdout.flush() - except (IOError,ValueError): + except (IOError, ValueError): pass # Put newline at end of string if line doesn't have one. if not s.endswith("\n"): @@ -224,13 +224,14 @@ def which(file, mode=os.F_OK | os.X_OK): debug2("which() could not find '%s' in %s" % (file, path)) return rv + def is_admin_user(): if sys.platform == 'win32': import ctypes # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script/41930586#41930586 try: return ctypes.windll.shell32.IsUserAnAdmin() - except: + except Exception: return False return os.getuid() == 0 diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 4c3cc712d..72f74e124 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -11,39 +11,42 @@ import time import traceback -try: - import pydivert -except ImportError: - raise Fatal('Could not import pydivert module. windivert requires https://pypi.org/project/pydivert') from sshuttle.methods import BaseMethod from sshuttle.helpers import debug3, log, debug1, debug2, Fatal -# https://reqrypt.org/windivert-doc.html#divert_iphdr +try: + # https://reqrypt.org/windivert-doc.html#divert_iphdr + import pydivert +except ImportError: + raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert") ConnectionTuple = namedtuple( - "ConnectionTuple", ["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", 'state'] + "ConnectionTuple", + ["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", "state"], ) WINDIVERT_MAX_CONNECTIONS = 10_000 + class IPProtocol(IntEnum): TCP = socket.IPPROTO_TCP UDP = socket.IPPROTO_UDP @property def filter(self): - return 'tcp' if self == IPProtocol.TCP else 'udp' + return "tcp" if self == IPProtocol.TCP else "udp" + class IPFamily(IntEnum): - IPv4 = socket.AF_INET - IPv6 = socket.AF_INET6 + IPv4 = socket.AF_INET + IPv6 = socket.AF_INET6 @property def filter(self): - return 'ip' if self == socket.AF_INET else 'ipv6' + return "ip" if self == socket.AF_INET else "ipv6" @property def version(self): @@ -51,14 +54,14 @@ def version(self): @property def loopback_addr(self): - return '127.0.0.1' if self == socket.AF_INET else '::1' + return "127.0.0.1" if self == socket.AF_INET else "::1" class ConnState(IntEnum): - TCP_SYN_SENT = 11 # SYN sent - TCP_ESTABLISHED = 12 # SYN+ACK received - TCP_FIN_WAIT_1 = 91 # FIN sent - TCP_CLOSE_WAIT = 92 # FIN received + TCP_SYN_SENT = 11 # SYN sent + TCP_ESTABLISHED = 12 # SYN+ACK received + TCP_FIN_WAIT_1 = 91 # FIN sent + TCP_CLOSE_WAIT = 92 # FIN received @staticmethod def can_timeout(state): @@ -70,27 +73,34 @@ def repr_pkt(p): if p.tcp: t = p.tcp r += f" {len(t.payload)}B (" - r += '+'.join(f.upper() for f in ('fin','syn', "rst", "psh", 'ack', 'urg', 'ece', 'cwr', 'ns') if getattr(t, f)) - r += f') SEQ#{t.seq_num}' + r += "+".join( + f.upper() for f in ("fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr", "ns") if getattr(t, f) + ) + r += f") SEQ#{t.seq_num}" if t.ack: - r += f' ACK#{t.ack_num}' - r += f' WZ={t.window_size}' + r += f" ACK#{t.ack_num}" + r += f" WZ={t.window_size}" else: r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}" return f"" + def synchronized_method(lock): def decorator(method): @wraps(method) def wrapped(self, *args, **kwargs): with getattr(self, lock): return method(self, *args, **kwargs) + return wrapped + return decorator + class ConnTrack: - _instance =None + _instance = None + def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = object.__new__(cls) @@ -98,13 +108,15 @@ def __new__(cls, *args, **kwargs): raise RuntimeError("ConnTrack can not be instantiated multiple times") def __init__(self, name, max_connections=0) -> None: - self.struct_full_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H', '16s', 'H', 'L', 'B'))) - self.struct_src_tuple = struct.Struct('>' + ''.join(('B', 'B', '16s', 'H'))) - self.struct_state_tuple = struct.Struct('>' + ''.join(('L', 'B'))) + self.struct_full_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B"))) + self.struct_src_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H"))) + self.struct_state_tuple = struct.Struct(">" + "".join(("L", "B"))) try: self.max_connections = max_connections - self.shm_list = shared_memory.ShareableList([bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name) + self.shm_list = shared_memory.ShareableList( + [bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name + ) self.is_owner = True self.next_slot = 0 self.used_slots = set() @@ -114,9 +126,12 @@ def __init__(self, name, max_connections=0) -> None: self.shm_list = shared_memory.ShareableList(name=name) self.max_connections = len(self.shm_list) - debug2(f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} shm_size={self.shm_list.shm.size}B") + debug2( + f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} " + f"shm_size={self.shm_list.shm.size}B" + ) - @synchronized_method('rlock') + @synchronized_method("rlock") def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") @@ -129,23 +144,26 @@ def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): for _ in range(self.max_connections): if self.next_slot not in self.used_slots: break - self.next_slot = (self.next_slot +1) % self.max_connections + self.next_slot = (self.next_slot + 1) % self.max_connections else: - raise RuntimeError("No slot available in ConnTrack") # should not be here + raise RuntimeError("No slot available in ConnTrack") # should not be here src_addr = ipaddress.ip_address(src_addr) dst_addr = ipaddress.ip_address(dst_addr) assert src_addr.version == dst_addr.version ip_version = src_addr.version - state_epoch = int(time.time()) + state_epoch = int(time.time()) entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state) packed = self.struct_full_tuple.pack(*entry) self.shm_list[self.next_slot] = packed self.used_slots.add(self.next_slot) proto = IPProtocol(proto) - debug3(f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to slot={self.next_slot} | #ActiveConn={len(self.used_slots)}") + debug3( + f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to " + f"slot={self.next_slot} | #ActiveConn={len(self.used_slots)}" + ) - @synchronized_method('rlock') + @synchronized_method("rlock") def update(self, proto, src_addr, src_port, state): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") @@ -155,12 +173,18 @@ def update(self, proto, src_addr, src_port, state): if self.shm_list[i].startswith(packed): state_epoch = int(time.time()) self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) - debug3(f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3( + f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | " + f"#ActiveConn={len(self.used_slots)}" + ) return self._unpack(self.shm_list[i]) else: - debug3(f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | #ActiveConn={len(self.used_slots)}") + debug3( + f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | " + f"#ActiveConn={len(self.used_slots)}" + ) - @synchronized_method('rlock') + @synchronized_method("rlock") def remove(self, proto, src_addr, src_port): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") @@ -169,13 +193,18 @@ def remove(self, proto, src_addr, src_port): for i in self.used_slots: if self.shm_list[i].startswith(packed): conn = self._unpack(self.shm_list[i]) - self.shm_list[i] = b'' + self.shm_list[i] = b"" self.used_slots.remove(i) - debug3(f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3( + f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | " + f"#ActiveConn={len(self.used_slots)}" + ) return conn else: - debug3(f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove | #ActiveConn={len(self.used_slots)}") - + debug3( + f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove |" + f" #ActiveConn={len(self.used_slots)}" + ) def get(self, proto, src_addr, src_port): src_addr = ipaddress.ip_address(src_addr) @@ -184,7 +213,7 @@ def get(self, proto, src_addr, src_port): if entry and entry.startswith(packed): return self._unpack(entry) - @synchronized_method('rlock') + @synchronized_method("rlock") def gc(self, connection_timeout_sec=15): now = int(time.time()) n = 0 @@ -195,47 +224,62 @@ def gc(self, connection_timeout_sec=15): continue if ConnState.can_timeout(state): conn = self._unpack(self.shm_list[i]) - self.shm_list[i] = b'' + self.shm_list[i] = b"" self.used_slots.remove(i) n += 1 - debug3(f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name}) from slot={i} | #ActiveConn={len(self.used_slots)}") + debug3( + f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name})" + f" from slot={i} | #ActiveConn={len(self.used_slots)}" + ) debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}") def _unpack(self, packed): - (proto, ip_version, src_addr_packed, src_port, dst_addr_packed, dst_port, state_epoch, state) = self.struct_full_tuple.unpack(packed) - dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) - src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) - return ConnectionTuple(IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state)) - + ( + proto, + ip_version, + src_addr_packed, + src_port, + dst_addr_packed, + dst_port, + state_epoch, + state, + ) = self.struct_full_tuple.unpack(packed) + dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) + src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) + return ConnectionTuple( + IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) + ) + def __iter__(self): def conn_iter(): for i in self.used_slots: yield self._unpack(self.shm_list[i]) + return conn_iter() def __repr__(self): - return f"" + return f"" class Method(BaseMethod): network_config = {} proxy_port = None - proxy_addr = { IPFamily.IPv4: None, IPFamily.IPv6: None } + proxy_addr = {IPFamily.IPv4: None, IPFamily.IPv6: None} def __init__(self, name): super().__init__(name) - def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user, tmark): - log( f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): + log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") if nslist or user or udp: raise NotImplementedError() family = IPFamily(family) - # using loopback proxy address never worked. See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) + # using loopback proxy address never worked. + # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) # As a workaround we use another interface ip instead. # self.proxy_addr[family] = family.loopback_addr for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): @@ -252,37 +296,37 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, for (_, mask, exclude, network_addr, fport, lport) in subnets: if exclude: continue - assert fport == 0, 'custom port range not supported' - assert lport == 0, 'custom port range not supported' + assert fport == 0, "custom port range not supported" + assert lport == 0, "custom port range not supported" subnet_addresses.append("%s/%s" % (network_addr, mask)) - self.network_config[family] = { - 'subnets': subnet_addresses, + self.network_config[family] = { + "subnets": subnet_addresses, "nslist": nslist, } - - def wait_for_firewall_ready(self): debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") - self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getppid()}', WINDIVERT_MAX_CONNECTIONS) + self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) methods = (self._egress_divert, self._ingress_divert, self._connection_gc) ready_events = [] for fn in methods: ev = threading.Event() ready_events.append(ev) + def _target(): try: fn(ev.set) - except: - debug2(f'thread {fn.__name__} exiting due to: ' + traceback.format_exc()) + except Exception: + debug2(f"thread {fn.__name__} exiting due to: " + traceback.format_exc()) sys.stdin.close() # this will exist main thread sys.stdout.close() + threading.Thread(name=fn.__name__, target=_target, daemon=True).start() for ev in ready_events: - if not ev.wait(5): # at most 5 sec - raise Fatal(f"timeout in wait_for_firewall_ready()") - + if not ev.wait(5): # at most 5 sec + raise Fatal("timeout in wait_for_firewall_ready()") + def restore_firewall(self, port, family, udp, user): pass @@ -294,17 +338,17 @@ def get_supported_features(self): return result def get_tcp_dstip(self, sock): - if not hasattr(self, 'conntrack'): - self.conntrack = ConnTrack(f'sshuttle-windivert-{os.getpid()}') + if not hasattr(self, "conntrack"): + self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}") - src_addr , src_port = sock.getpeername() - c = self.conntrack.get(IPProtocol.TCP , src_addr, src_port) + src_addr, src_port = sock.getpeername() + c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port) if not c: - return (src_addr , src_port) + return (src_addr, src_port) return (c.dst_addr, c.dst_port) def is_supported(self): - if sys.platform == 'win32': + if sys.platform == "win32": return True return False @@ -315,8 +359,8 @@ def _egress_divert(self, ready_cb): # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: family_filters = [] for af, c in self.network_config.items(): - subnet_filters = [] - for cidr in c['subnets']: + subnet_filters = [] + for cidr in c["subnets"]: ip_network = ipaddress.ip_network(cidr) first_ip = ip_network.network_address last_ip = ip_network.broadcast_address @@ -333,11 +377,21 @@ def _egress_divert(self, ready_cb): proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] for pkt in w: debug3(">>> " + repr_pkt(pkt)) - if pkt.tcp.syn and not pkt.tcp.ack: # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) - self.conntrack.add(socket.IPPROTO_TCP, pkt.src_addr, pkt.src_port, pkt.dst_addr, pkt.dst_port, ConnState.TCP_SYN_SENT) - if pkt.tcp.fin: # FIN sent (start of graceful close our side, and we wait for ACK) + if pkt.tcp.syn and not pkt.tcp.ack: + # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) + self.conntrack.add( + socket.IPPROTO_TCP, + pkt.src_addr, + pkt.src_port, + pkt.dst_addr, + pkt.dst_port, + ConnState.TCP_SYN_SENT, + ) + if pkt.tcp.fin: + # FIN sent (start of graceful close our side, and we wait for ACK) self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1) - if pkt.tcp.rst : # RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply) + if pkt.tcp.rst: + # RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply) self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) # DNAT @@ -347,18 +401,19 @@ def _egress_divert(self, ready_cb): pkt.dst_addr = proxy_addr_ipv6 pkt.tcp.dst_port = proxy_port - # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well by setting src_addr to loopback address. - # Otherwise injecting packet will be ignored by Windows network stack as teh packet has to cross public to private address space. + # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well + # by setting src_addr to loopback address. + # Otherwise injecting packet will be ignored by Windows network stack + # as they packet has to cross public to private address space. # See: https://github.com/basil00/Divert/issues/82 # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. - # >>> pkt.dst_addr = proxy_addr_ipv4 + # >>> pkt.dst_addr = proxy_addr_ipv4 w.send(pkt, recalculate_checksum=True) - def _ingress_divert(self, ready_cb): proto = IPProtocol.TCP - direction = 'inbound' # only when proxy address is not loopback address (Useful for testing) + direction = "inbound" # only when proxy address is not loopback address (Useful for testing) ip_filters = [] for addr in (ipaddress.ip_address(a) for a in self.proxy_addr.values() if a): if addr.is_loopback: # Windivert treats all loopback traffic as outbound @@ -374,19 +429,23 @@ def _ingress_divert(self, ready_cb): ready_cb() for pkt in w: debug3("<<< " + repr_pkt(pkt)) - if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK received (connection established) + if pkt.tcp.syn and pkt.tcp.ack: + # SYN+ACK received (connection established) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) - elif pkt.tcp.rst: # RST received - Abrupt connection teardown initiated by other side. We don't expect anymore packets + elif pkt.tcp.rst: + # RST received - Abrupt connection teardown initiated by otherside. We don't expect anymore packets conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) # https://wiki.wireshark.org/TCP-4-times-close.md - elif pkt.tcp.fin and pkt.tcp.ack: # FIN+ACK received (Passive close by other side. We don't expect any more packets. Other side expects an ACK) + elif pkt.tcp.fin and pkt.tcp.ack: + # FIN+ACK received (Passive close by otherside. We don't expect any more packets. Otherside expects an ACK) conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) - elif pkt.tcp.fin: # FIN received (Other side initiated graceful close. We expects a final ACK for a FIN packet) + elif pkt.tcp.fin: + # FIN received (Otherside initiated graceful close. We expects a final ACK for a FIN packet) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT) else: conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: - debug2("Unexpected packet: " + repr_pkt(pkt)) + debug2("Unexpected packet: " + repr_pkt(pkt)) continue pkt.src_addr = conn.dst_addr pkt.tcp.src_port = conn.dst_port diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 6b947893a..bede68aa2 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -175,7 +175,7 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): # case, sshuttle might not work at all since it is not # possible to run python on the remote machine---even if # it is present. - devnull='/dev/null' + devnull = '/dev/null' pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s; exit 97") % \ (devnull, quote(pyscript)) @@ -204,9 +204,9 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path())) argv[0] = abs_path - if sys.platform != 'win32': (s1, s2) = socket.socketpair() + def preexec_fn(): # runs in the child process s2.close() @@ -222,13 +222,14 @@ def get_serversock(): preexec_fn = None pstdin = ssubprocess.PIPE pstdout = ssubprocess.PIPE + def get_serversock(): import threading def stream_stdout_to_sock(): try: fd = p.stdout.fileno() - for data in iter(lambda:os.read(fd, 16384), b''): + for data in iter(lambda: os.read(fd, 16384), b''): s1.sendall(data) debug3(f"<<<<< p.stdout.read() {len(data)} {data[:min(32,len(data))]}...") finally: @@ -238,7 +239,7 @@ def stream_stdout_to_sock(): def stream_sock_to_stdin(): try: - for data in iter(lambda:s1.recv(16384), b''): + for data in iter(lambda: s1.recv(16384), b''): debug3(f">>>>> p.stdout.write() {len(data)} {data[:min(32,len(data))]}...") while data: n = p.stdin.write(data) @@ -247,7 +248,7 @@ def stream_sock_to_stdin(): debug2("Thread 'stream_sock_to_stdin' exiting") s1.close() p.terminate() - + threading.Thread(target=stream_stdout_to_sock, name='stream_stdout_to_sock', daemon=True).start() threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() # s2.setblocking(False) @@ -258,7 +259,7 @@ def stream_sock_to_stdin(): debug2("executing: %r" % argv) p = ssubprocess.Popen(argv, stdin=pstdin, stdout=pstdout, preexec_fn=preexec_fn, - close_fds=close_fds, stderr=stderr, bufsize=0) + close_fds=close_fds, stderr=stderr, bufsize=0) serversock = get_serversock() serversock.sendall(content) diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index 29f306465..cb15f9fef 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -477,11 +477,9 @@ def fill(self): # If LATENCY_BUFFER_SIZE is inappropriately large, we will # get a MemoryError here. Read no more than 1MiB. if sys.platform == 'win32': - read = _nb_clean(self.rfile.raw._sock.recv, - min(1048576, LATENCY_BUFFER_SIZE)) + read = _nb_clean(self.rfile.raw._sock.recv, min(1048576, LATENCY_BUFFER_SIZE)) else: - read = _nb_clean(os.read, self.rfile.fileno(), - min(1048576, LATENCY_BUFFER_SIZE)) + read = _nb_clean(os.read, self.rfile.fileno(), min(1048576, LATENCY_BUFFER_SIZE)) except OSError: _, e = sys.exc_info()[:2] raise Fatal('other end: %r' % e) From 0c4c06112360b9d994e7be617e971db20c70c629 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 179/275] fix failing tests --- tests/client/test_firewall.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index f82ddfa5d..e714c81c4 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -157,6 +157,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): None, None, '0x01'), + call().wait_for_firewall_ready(), call().restore_firewall(1024, AF_INET6, True, None, None), call().restore_firewall(1025, AF_INET, True, None, None), ] From 2f88fc93cf944f555b4fc6b9d9fb5f08635bd608 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:26:21 +0530 Subject: [PATCH 180/275] add some comments --- sshuttle/client.py | 6 ++++-- sshuttle/firewall.py | 4 ++-- sshuttle/helpers.py | 2 +- sshuttle/ssh.py | 16 +++++++++++----- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index a1578dfe7..875f1f533 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -226,7 +226,10 @@ def __init__(self, method_name, sudo_pythonpath): argv_tries.append(argvbase) # runas_path = which("runas") # if runas_path: - # argv_tries.append(['runas' , '/noprofile', '/user:Administrator', 'python']) + # argv_tries.append([runas_path , '/noprofile', '/user:Administrator', 'python']) + # XXX:attempt to elevate privilege using 'runas' in windows seems not working. + # This is due to underlying ShellExecute() Windows api does not allow child process to inherit stdio. + # TODO(nom3ad): try to implement another way to achieve this. else: # Linux typically uses sudo; OpenBSD uses doas. However, some # Linux distributions are starting to use doas. @@ -303,7 +306,6 @@ def get_pfile(): socket_share_data = s1.share(self.p.pid) s1.close() socket_share_data_b64 = base64.b64encode(socket_share_data) - # debug3(f"{socket_share_data_b64=}") self.p.stdin.write(socket_share_data_b64 + b'\n') self.p.stdin.flush() return s2.makefile('rwb') diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 5193b15dc..4134e3d85 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -328,6 +328,8 @@ def main(method_name, syslog): user, group, tmark) try: + # For some methods (eg: windivert) firewall setup will be differed / will run asynchronously. + # Such method implements wait_for_firewall_ready() to wait until firewall is up and running. method.wait_for_firewall_ready() except NotImplementedError: pass @@ -347,9 +349,7 @@ def main(method_name, syslog): # authentication at shutdown time - that cleanup is important! while 1: try: - debug3("===================================================") line = stdin.readline(128) - debug3("===================================================" + str(line)) except IOError as e: debug3('read from stdin failed: %s' % (e,)) return diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 4dca60eb4..979c13129 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -227,8 +227,8 @@ def which(file, mode=os.F_OK | os.X_OK): def is_admin_user(): if sys.platform == 'win32': - import ctypes # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script/41930586#41930586 + import ctypes try: return ctypes.windll.shell32.IsUserAnAdmin() except Exception: diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index bede68aa2..3ffb045f1 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -206,11 +206,11 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): if sys.platform != 'win32': (s1, s2) = socket.socketpair() + pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno()) def preexec_fn(): # runs in the child process s2.close() - pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno()) s1.close() def get_serversock(): @@ -218,11 +218,18 @@ def get_serversock(): os.close(pstdout) return s2 else: + # In Windows python implementation it seems not possible to use sockets as subprocess stdio + # Also select.select() won't work on pipes. + # So we have to use both socketpair and pipes together along with reader/writer threads to + # stream data between them + # NOTE: Their can be a way to use sockets as stdio with some hacks. + # https://stackoverflow.com/questions/4993119/redirect-io-of-process-to-windows-socket (s1, s2) = socket.socketpair() - preexec_fn = None pstdin = ssubprocess.PIPE pstdout = ssubprocess.PIPE + preexec_fn = None + def get_serversock(): import threading @@ -231,7 +238,7 @@ def stream_stdout_to_sock(): fd = p.stdout.fileno() for data in iter(lambda: os.read(fd, 16384), b''): s1.sendall(data) - debug3(f"<<<<< p.stdout.read() {len(data)} {data[:min(32,len(data))]}...") + # debug3(f"<<<<< p.stdout.read() {len(data)} {data[:min(32,len(data))]}...") finally: debug2("Thread 'stream_stdout_to_sock' exiting") s1.close() @@ -240,7 +247,7 @@ def stream_stdout_to_sock(): def stream_sock_to_stdin(): try: for data in iter(lambda: s1.recv(16384), b''): - debug3(f">>>>> p.stdout.write() {len(data)} {data[:min(32,len(data))]}...") + # debug3(f">>>>> p.stdout.write() {len(data)} {data[:min(32,len(data))]}...") while data: n = p.stdin.write(data) data = data[n:] @@ -251,7 +258,6 @@ def stream_sock_to_stdin(): threading.Thread(target=stream_stdout_to_sock, name='stream_stdout_to_sock', daemon=True).start() threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() - # s2.setblocking(False) return s2 # https://stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows From 3f34e27a2c139b01bf0fe5663e95937c887679a7 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Sun, 6 Nov 2022 14:03:54 +0530 Subject: [PATCH 181/275] try not use socket share --- sshuttle/client.py | 38 ++++++++++++++++++++++++-------------- sshuttle/firewall.py | 23 ++++++++++++++++------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 875f1f533..c097086a6 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -223,13 +223,13 @@ def __init__(self, method_name, sudo_pythonpath): argv_tries.append(argvbase) else: if sys.platform == 'win32': - argv_tries.append(argvbase) # runas_path = which("runas") # if runas_path: # argv_tries.append([runas_path , '/noprofile', '/user:Administrator', 'python']) - # XXX:attempt to elevate privilege using 'runas' in windows seems not working. - # This is due to underlying ShellExecute() Windows api does not allow child process to inherit stdio. - # TODO(nom3ad): try to implement another way to achieve this. + # XXX: Attempt to elevate privilege using 'runas' in windows seems not working. + # Because underlying ShellExecute() Windows api does not allow child process to inherit stdio. + # TODO(nom3ad): Try to implement another way to achieve this. + raise Fatal("Privilege elevation for Windows is not yet implemented. Please run from an administrator shell") else: # Linux typically uses sudo; OpenBSD uses doas. However, some # Linux distributions are starting to use doas. @@ -294,22 +294,32 @@ def get_pfile(): return s2.makefile('rwb') else: - (s1, s2) = socket.socketpair() - pstdout = None + # In windows, if client/firewall processes is running as admin user, stdio can be used for communication. + # But if firewall process is run with elevated mode, access to stdio is lost. + # So we have to use a socketpair (as in unix). + # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows + can_use_stdio = is_admin_user() + pstdout = ssubprocess.PIPE if can_use_stdio else None pstdin = ssubprocess.PIPE preexec_fn = None penv = os.environ.copy() penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) def get_pfile(): - import base64 - socket_share_data = s1.share(self.p.pid) - s1.close() - socket_share_data_b64 = base64.b64encode(socket_share_data) - self.p.stdin.write(socket_share_data_b64 + b'\n') - self.p.stdin.flush() - return s2.makefile('rwb') - + if can_use_stdio: + import io + self.p.stdin.write(b'STDIO:\n') + self.p.stdin.flush() + return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) + else: + import base64 + (s1, s2) = socket.socketpair() + socket_share_data = s1.share(self.p.pid) + s1.close() + socket_share_data_b64 = base64.b64encode(socket_share_data) + self.p.stdin.write(b'SOCKETSHARE:' + socket_share_data_b64 + b'\n') + self.p.stdin.flush() + return s2.makefile('rwb') try: debug1("Starting firewall manager with command: %r" % argv) self.p = ssubprocess.Popen(argv, stdout=pstdout, stdin=pstdin, env=penv, diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 4134e3d85..da6145639 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -121,13 +121,22 @@ def _setup_daemon_windows(): signal.signal(signal.SIGTERM, firewall_exit) signal.signal(signal.SIGINT, firewall_exit) - socket_share_data_b64 = sys.stdin.readline() - # debug3(f'FROM_SHARE ${socket_share_data_b64=}') - socket_share_data = base64.b64decode(socket_share_data_b64) - sock = socket.fromshare(socket_share_data) - sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) - sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) - sock.close() + + socket_share_data_prefix = 'SOCKETSHARE:' + line = sys.stdin.readline().strip() + if line.startswith('SOCKETSHARE:'): + debug3('Using shared socket for communicating with sshuttle client process') + socket_share_data_b64 = line[len(socket_share_data_prefix):] + socket_share_data = base64.b64decode(socket_share_data_b64) + sock = socket.fromshare(socket_share_data) + sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) + sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) + sock.close() + elif line.startswith("STDIO:"): + debug3('Using inherited stdio for communicating with sshuttle client process') + else: + raise Fatal("Unexpected stdin: " + line) + return sys.stdin, sys.stdout From 9c5517fd25f17130b7da46d8acb1e0f02c961cda Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Sun, 6 Nov 2022 15:05:32 +0530 Subject: [PATCH 182/275] use custom RWPair instead of io.BufferedRWPair --- sshuttle/client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index c097086a6..0523fde72 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -310,7 +310,22 @@ def get_pfile(): import io self.p.stdin.write(b'STDIO:\n') self.p.stdin.flush() - return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) + class RWPair: + def __init__(self, r, w): + self.r = r + self.w = w + self.read = r.read + self.readline = r.readline + self.write = w.write + self.flush = w.flush + def close(self): + for f in self.r, self.w: + try: + f.close() + except: + pass + return RWPair(self.p.stdout, self.p.stdin) + # return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) else: import base64 (s1, s2) = socket.socketpair() From 7b8f140870b8e2432853fa8b997be651efb54f75 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Sun, 6 Nov 2022 17:16:15 +0530 Subject: [PATCH 183/275] ensure non loopback address for windivert method --- sshuttle/client.py | 29 +++++++++++++++++++---------- sshuttle/methods/windivert.py | 28 ++++++++++++++++++++++++---- sshuttle/ssh.py | 2 +- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index 0523fde72..20e5e4139 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -295,10 +295,10 @@ def get_pfile(): else: # In windows, if client/firewall processes is running as admin user, stdio can be used for communication. - # But if firewall process is run with elevated mode, access to stdio is lost. + # But if firewall process is run with elevated mode, access to stdio is lost. # So we have to use a socketpair (as in unix). - # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows - can_use_stdio = is_admin_user() + # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows + can_use_stdio = is_admin_user() pstdout = ssubprocess.PIPE if can_use_stdio else None pstdin = ssubprocess.PIPE preexec_fn = None @@ -306,25 +306,27 @@ def get_pfile(): penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) def get_pfile(): - if can_use_stdio: - import io + if can_use_stdio: self.p.stdin.write(b'STDIO:\n') self.p.stdin.flush() + class RWPair: def __init__(self, r, w): - self.r = r + self.r = r self.w = w self.read = r.read self.readline = r.readline self.write = w.write self.flush = w.flush + def close(self): for f in self.r, self.w: try: f.close() - except: + except Exception: pass return RWPair(self.p.stdout, self.p.stdin) + # import io # return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) else: import base64 @@ -885,7 +887,11 @@ def main(listenip_v6, listenip_v4, # listenip_v4 contains user specified value or it is set to "auto". if listenip_v4 == "auto": - listenip_v4 = ('127.0.0.1', 0) + if sys.platform == 'win32': + listenip_v4 = ('0.0.0.0', 0) # windivert method won't work with loopback interface + else: + listenip_v4 = ('127.0.0.1', 0) + debug1("Using default IPv4 listen address " + listenip_v4[0]) # listenip_v6 is... # None when IPv6 is disabled. @@ -895,8 +901,11 @@ def main(listenip_v6, listenip_v4, debug1("IPv6 disabled by --disable-ipv6") if listenip_v6 == "auto": if avail.ipv6: - debug1("IPv6 enabled: Using default IPv6 listen address ::1") - listenip_v6 = ('::1', 0) + if sys.platform == 'win32': + listenip_v6 = ('::', 0) # windivert method won't work with loopback interface + else: + listenip_v6 = ('::1', 0) + debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0]) else: debug1("IPv6 disabled since it isn't supported by method " "%s." % fw.method.name) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 72f74e124..da8c6bcc5 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -4,6 +4,8 @@ import threading from collections import namedtuple import socket +import subprocess +import re from multiprocessing import shared_memory import struct from functools import wraps @@ -270,6 +272,18 @@ class Method(BaseMethod): def __init__(self, name): super().__init__(name) + def _get_local_proxy_listen_addr(self, port, family): + proto = "TCPv6" if family.version == 6 else "TCP" + for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): + try: + _, local_addr, _, state, *_ = re.split(r"\s+", line.strip()) + except ValueError: + continue + port_suffix = ":" + str(port) + if state == "LISTENING" and local_addr.endswith(port_suffix): + return ipaddress.ip_address(local_addr[:-len(port_suffix)].strip("[]")) + raise Fatal("Could not find listening address for {}/{}".format(port, proto)) + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") @@ -279,16 +293,20 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmar family = IPFamily(family) # using loopback proxy address never worked. + # >>> self.proxy_addr[family] = family.loopback_addr # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) # As a workaround we use another interface ip instead. - # self.proxy_addr[family] = family.loopback_addr + + local_addr = self._get_local_proxy_listen_addr(port, family) for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): if addr.is_loopback or addr.version != family.version: continue - self.proxy_addr[family] = str(addr) - break + if local_addr.is_unspecified or local_addr == addr: + debug2("Found non loopback address to connect to proxy: " + str(addr)) + self.proxy_addr[family] = str(addr) + break else: - raise Fatal(f"Could not find a non loopback proxy address for {family.name}") + raise Fatal("Windivert method requires proxy to listen on non loopback address") self.proxy_port = port @@ -423,6 +441,8 @@ def _ingress_divert(self, ready_cb): else: # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this ip_filters.append(f"ipv6.SrcAddr=={addr}") + if not ip_filters: + raise Fatal("At least ipv4 or ipv6 address is expected") filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" debug2(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter) as w: diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 3ffb045f1..ee5a47ebb 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -12,7 +12,7 @@ from urllib.parse import urlparse import sshuttle.helpers as helpers -from sshuttle.helpers import debug2, debug3, which, get_path, Fatal +from sshuttle.helpers import debug2, which, get_path, Fatal def get_module_source(name): From 49f46cd528429c354be0bf85bedf3413c9aee806 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:51:22 +0530 Subject: [PATCH 184/275] Add containers based testbed setup --- hack/Containerfile | 15 ++++++++++++++ hack/compose.yml | 29 +++++++++++++++++++++++++++ hack/exec-iperf | 15 ++++++++++++++ hack/exec-sshuttle | 29 +++++++++++++++++++++++++++ hack/setup.service | 49 ++++++++++++++++++++++++++++++++++++++++++++++ hack/test-bed-up | 9 +++++++++ 6 files changed, 146 insertions(+) create mode 100644 hack/Containerfile create mode 100644 hack/compose.yml create mode 100755 hack/exec-iperf create mode 100755 hack/exec-sshuttle create mode 100755 hack/setup.service create mode 100755 hack/test-bed-up diff --git a/hack/Containerfile b/hack/Containerfile new file mode 100644 index 000000000..29e63052f --- /dev/null +++ b/hack/Containerfile @@ -0,0 +1,15 @@ +FROM docker.io/linuxserver/openssh-server:latest +# https://hub.docker.com/r/linuxserver/openssh-server/ + +RUN apk add --no-cache bash python3 nginx iperf3 + +# suppress linuxserver.io logo printing +RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run + +ENV PUID=1000 +ENV PGID=1000 +ENV PASSWORD_ACCESS=true +ENV USER_NAME=test +ENV USER_PASSWORD=test +ENV LOG_STDOUT=true +COPY ./setup.service /etc/services.d/setup.service/run \ No newline at end of file diff --git a/hack/compose.yml b/hack/compose.yml new file mode 100644 index 000000000..59fe7d576 --- /dev/null +++ b/hack/compose.yml @@ -0,0 +1,29 @@ +name: sshuttle-testbed + +services: + node-1: + image: ghcr.io/sshuttle/sshuttle-testbed + container_name: sshuttle-testbed-node-1 + hostname: node-1 + ports: + - 22001:2222 + cap_add: + - "NET_ADMIN" + environment: + - IP_ADDRESSES=10.55.1.77/24 + node-2: + image: ghcr.io/sshuttle/sshuttle-testbed + container_name: sshuttle-testbed-node-2 + hostname: node-2 + ports: + - 22002:2222 + cap_add: + - "NET_ADMIN" + environment: + - IP_ADDRESSES=10.55.2.77/32 + +networks: + default: + driver: bridge + enable_ipv6: true + internal: true \ No newline at end of file diff --git a/hack/exec-iperf b/hack/exec-iperf new file mode 100755 index 000000000..14b47d7a6 --- /dev/null +++ b/hack/exec-iperf @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + + +node=$1 + +if [[ ! $node =~ [1-9]+ ]]; then + echo "node argument missing. should be '1' , '2' etc" + exit 2 +fi +shift + +ip="10.55.$node.77" + +exec iperf3 --client "$ip" --port 5001 diff --git a/hack/exec-sshuttle b/hack/exec-sshuttle new file mode 100755 index 000000000..5d5f692d7 --- /dev/null +++ b/hack/exec-sshuttle @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +node=$1 + +if [[ ! $node =~ [1-9]+ ]]; then + echo "node argument missing. should be '1' , '2' etc" + exit 2 +fi +shift + +port="2200$node" +subnet_args="-N" +host=localhost +user="test:test" + +if ! command -v sshpass >/dev/null; then + echo "sshpass is not found. You have to manually enter ssh password: 'test'" >&2 + user="test" +fi +ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + +if [[ "$*" =~ -v ]]; then + ssh_cmd+='-v' +fi + +SSHUTTLE_BIN=${SSHUTTLE_BIN:-"$(dirname "$0")/../run"} +set -x +exec "${SSHUTTLE_BIN}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "$@" $subnet_args diff --git a/hack/setup.service b/hack/setup.service new file mode 100755 index 000000000..95e55c534 --- /dev/null +++ b/hack/setup.service @@ -0,0 +1,49 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +set -e + +echo -e ">>> Setting up $(hostname) | id: $(id) | $(python --version) \nip: $(ip a)\n route: $(ip r)" + +iface="$(ip route | awk '/default/ { print $5 }')" +default_gw="$(ip route | awk '/default/ { print $3 }')" +for addr in ${IP_ADDRESSES//,/ }; do + echo ">>> Adding $addr to interface $iface" + net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}') + ( + set -ex + ip addr add "$addr" dev "$iface" + ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes + ) +done + +echo ">>> Starting iperf3 server" +iperf3 --server --port 5001 & + +mkdir -p /www +echo -e "
Hello from $(hostname)
+
+ip address
+$(ip address)
+ip route
+$(ip route)
+
" >/www/index.html +echo " +daemon off; +worker_processes 1; +error_log /dev/stdout info; +events { + worker_connections 1024; +} +http { + include /etc/nginx/mime.types; + server { + access_log /dev/stdout; + listen 8080 default_server; + listen [::]:8080 default_server; + root /www; + } +}" >/etc/nginx/nginx.conf + +echo ">>> Starting nginx" +exec nginx diff --git a/hack/test-bed-up b/hack/test-bed-up new file mode 100755 index 000000000..d23ade474 --- /dev/null +++ b/hack/test-bed-up @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" + +# podman build -t ghcr.io/sshuttle/sshuttle-testbed . +# podman-compose up + +docker build -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile . +docker compose up \ No newline at end of file From 900acc3ac7440461d6e101e15bcbde3d09cc51b4 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Mon, 1 Jan 2024 22:12:17 +0530 Subject: [PATCH 185/275] refactoring to make it better structured --- hack/compose.yml | 3 +-- hack/exec-iperf | 15 ----------- hack/exec-tool | 37 ++++++++++++++++++++++++++ hack/run-benchmark | 40 ++++++++++++++++++++++++++++ hack/setup.service | 14 ++++++---- hack/test-bed | 30 +++++++++++++++++++++ hack/test-bed-up | 9 ------- sshuttle/__main__.py | 5 +++- sshuttle/assembler.py | 16 +++++++----- sshuttle/client.py | 21 +++++---------- sshuttle/firewall.py | 35 +++++++++++++------------ sshuttle/helpers.py | 27 ++++++++++++++++--- sshuttle/methods/__init__.py | 1 + sshuttle/methods/windivert.py | 3 ++- sshuttle/options.py | 7 ++++- sshuttle/server.py | 2 +- sshuttle/ssh.py | 30 +++++++++++---------- sshuttle/ssnet.py | 49 +++++------------------------------ 18 files changed, 213 insertions(+), 131 deletions(-) delete mode 100755 hack/exec-iperf create mode 100755 hack/exec-tool create mode 100755 hack/run-benchmark create mode 100755 hack/test-bed delete mode 100755 hack/test-bed-up diff --git a/hack/compose.yml b/hack/compose.yml index 59fe7d576..e008e6d28 100644 --- a/hack/compose.yml +++ b/hack/compose.yml @@ -25,5 +25,4 @@ services: networks: default: driver: bridge - enable_ipv6: true - internal: true \ No newline at end of file + # internal: true \ No newline at end of file diff --git a/hack/exec-iperf b/hack/exec-iperf deleted file mode 100755 index 14b47d7a6..000000000 --- a/hack/exec-iperf +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -e - - -node=$1 - -if [[ ! $node =~ [1-9]+ ]]; then - echo "node argument missing. should be '1' , '2' etc" - exit 2 -fi -shift - -ip="10.55.$node.77" - -exec iperf3 --client "$ip" --port 5001 diff --git a/hack/exec-tool b/hack/exec-tool new file mode 100755 index 000000000..47b84cae0 --- /dev/null +++ b/hack/exec-tool @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e + +tool=${1?:"tool argument missing. should be one of iperf3,ping,curl,ab"} +node=${2?:"node argument missing. should be '1' , '2' etc"} +shift 2 + +ip="10.55.$node.77" +connect_timeout_sec=3 + +function with_set_x() { + set -x + "$@" + { + ec=$? + set +x + return $ec + } 2>/dev/null +} + +case "$tool" in +ping) + with_set_x exec ping -W $connect_timeout_sec "$@" "$ip" + ;; +iperf3) + port=5001 + with_set_x exec iperf3 --client "$ip" --port=$port --connect-timeout=$connect_timeout_sec "$@" + ;; +curl) + port=8080 + with_set_x exec curl "http://$ip:$port/" -v --connect-timeout $connect_timeout_sec "$@" + ;; +ab) + port=8080 + with_set_x exec ab -n 100 -c 20 -s $connect_timeout_sec "$@" "http://$ip:$port/" + ;; +esac diff --git a/hack/run-benchmark b/hack/run-benchmark new file mode 100755 index 000000000..2ba4386a3 --- /dev/null +++ b/hack/run-benchmark @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" + +function with_set_x() { + set -x + "$@" + { ec=$?; set +x;return $ec; } 2>/dev/null +} + + +./test-bed up -d + + + +benchmark() { + local sshuttle_bin="${1?:}" + echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin ========" + if [[ "$sshuttle_bin" == dev ]]; then + sshuttle_bin="../run" + fi + SSHUTTLE_BIN=$sshuttle_bin ./exec-sshuttle 1 --listen 55771 & + sshuttle_pid=$! + trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT + while ! nc -z localhost 55771; do sleep 0.1; done + sleep 1 + ./exec-tool iperf3 1 --time=4 + with_set_x kill -15 $sshuttle_pid + wait $sshuttle_pid || true +} + + +if [[ "$1" ]]; then + benchmark "$1" +else + benchmark "${SSHUTTLE_BIN:-/bin/sshuttle}" + benchmark dev +fi + + diff --git a/hack/setup.service b/hack/setup.service index 95e55c534..8a6b0b740 100755 --- a/hack/setup.service +++ b/hack/setup.service @@ -5,16 +5,20 @@ set -e echo -e ">>> Setting up $(hostname) | id: $(id) | $(python --version) \nip: $(ip a)\n route: $(ip r)" +function with_set_x() { + set -x + "$@" + { ec=$?; set +x;return $ec; } 2>/dev/null +} + + iface="$(ip route | awk '/default/ { print $5 }')" default_gw="$(ip route | awk '/default/ { print $3 }')" for addr in ${IP_ADDRESSES//,/ }; do echo ">>> Adding $addr to interface $iface" net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}') - ( - set -ex - ip addr add "$addr" dev "$iface" - ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes - ) + with_set_x ip addr add "$addr" dev "$iface" + with_set_x ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes done echo ">>> Starting iperf3 server" diff --git a/hack/test-bed b/hack/test-bed new file mode 100755 index 000000000..dddb04fe3 --- /dev/null +++ b/hack/test-bed @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" + + +if [[ -z $1 || $1 = -* ]]; then + set -- up "$@" +fi + +function with_set_x() { + set -x + "$@" + { ec=$?; set +x;return $ec; } 2>/dev/null +} + +function build() { + # podman build -t ghcr.io/sshuttle/sshuttle-testbed . + with_set_x docker build -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile . +} + +function compose() { + # podman-compose "$@" + with_set_x docker compose "$@" +} + + +if [[ $* = *--build* ]]; then + build +fi +compose "$@" diff --git a/hack/test-bed-up b/hack/test-bed-up deleted file mode 100755 index d23ade474..000000000 --- a/hack/test-bed-up +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -e -cd "$(dirname "$0")" - -# podman build -t ghcr.io/sshuttle/sshuttle-testbed . -# podman-compose up - -docker build -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile . -docker compose up \ No newline at end of file diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index 327956b66..c7566790d 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -3,6 +3,9 @@ import os from sshuttle.cmdline import main from sshuttle.helpers import debug3 +from sshuttle import __version__ + +debug3("Starting cmd %r (pid:%s) | sshuttle: %s | Python: %s" % (sys.argv, os.getpid(), __version__, sys.version)) exit_code = main() -debug3("Exiting process %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) +debug3("Exiting cmd %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) sys.exit(exit_code) diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 3cffdee97..e944280da 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -3,24 +3,27 @@ import types import platform -verbosity = verbosity # noqa: F821 must be a previously defined global +stdin = stdin # type: typing.BinaryIO # noqa: F821 must be a previously defined global +verbosity = verbosity # type: int # noqa: F821 must be a previously defined global if verbosity > 0: sys.stderr.write(' s: Running server on remote host with %s (version %s)\n' % (sys.executable, platform.python_version())) + z = zlib.decompressobj() + while 1: - name = sys.stdin.readline().strip() + name = stdin.readline().strip() if name: - # python2 compat: in python2 sys.stdin.readline().strip() -> str - # in python3 sys.stdin.readline().strip() -> bytes + # python2 compat: in python2 stdin.readline().strip() -> str + # in python3 stdin.readline().strip() -> bytes # (see #481) if sys.version_info >= (3, 0): name = name.decode("ASCII") - nbytes = int(sys.stdin.readline()) + nbytes = int(stdin.readline()) if verbosity >= 2: sys.stderr.write(' s: assembling %r (%d bytes)\n' % (name, nbytes)) - content = z.decompress(sys.stdin.read(nbytes)) + content = z.decompress(stdin.read(nbytes)) module = types.ModuleType(name) parents = name.rsplit(".", 1) @@ -44,6 +47,7 @@ import sshuttle.cmdline_options as options # noqa: E402 from sshuttle.server import main # noqa: E402 + main(options.latency_control, options.latency_buffer_size, options.auto_hosts, options.to_nameserver, options.auto_nets) diff --git a/sshuttle/client.py b/sshuttle/client.py index 20e5e4139..e2179f441 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -391,14 +391,14 @@ def close(self): 'Command=%r' % (skipped_text, self.argv)) continue - method_name = line.strip()[6:] + method_name = line[6:-1] self.method = get_method(method_name.decode("ASCII")) self.method.set_firewall(self) success = True break if not success: - raise Fatal("All attempts to run firewall client with elevated privileges were failed.") + raise Fatal("All attempts to run firewall client process with elevated privileges were failed.") def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, @@ -461,9 +461,9 @@ def start(self): (udp, user, group, bytes(self.tmark, 'ascii'), os.getpid())) self.pfile.flush() - line = self.pfile.readline().strip() + line = self.pfile.readline() self.check() - if line != b'STARTED': + if line != b'STARTED\n': raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) def sethostip(self, hostname, ip): @@ -615,7 +615,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, debug1('Connecting to server...') try: - (serverproc, serversock) = ssh.connect( + (serverproc, rfile, wfile) = ssh.connect( ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, add_cmd_delimiter=add_cmd_delimiter, @@ -630,7 +630,6 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, raise Fatal("failed to establish ssh session (1)") else: raise - rfile, wfile = serversock.makefile("rb"), serversock.makefile("wb") mux = Mux(rfile, wfile) handlers.append(mux) @@ -887,10 +886,7 @@ def main(listenip_v6, listenip_v4, # listenip_v4 contains user specified value or it is set to "auto". if listenip_v4 == "auto": - if sys.platform == 'win32': - listenip_v4 = ('0.0.0.0', 0) # windivert method won't work with loopback interface - else: - listenip_v4 = ('127.0.0.1', 0) + listenip_v4 = ('127.0.0.1' if avail.loopback_port else '0.0.0.0', 0) debug1("Using default IPv4 listen address " + listenip_v4[0]) # listenip_v6 is... @@ -901,10 +897,7 @@ def main(listenip_v6, listenip_v4, debug1("IPv6 disabled by --disable-ipv6") if listenip_v6 == "auto": if avail.ipv6: - if sys.platform == 'win32': - listenip_v6 = ('::', 0) # windivert method won't work with loopback interface - else: - listenip_v6 = ('::1', 0) + listenip_v6 = ('::1' if avail.loopback_port else '::', 0) debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0]) else: debug1("IPv6 disabled since it isn't supported by method " diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index da6145639..44276b9f3 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -84,14 +84,17 @@ def firewall_exit(signum, frame): # the typical exit process as described above. global sshuttle_pid if sshuttle_pid: - debug1("Relaying SIGINT to sshuttle process %d\n" % sshuttle_pid) - os.kill(sshuttle_pid, signal.SIGINT) + debug1("Relaying interupt signal to sshuttle process %d\n" % sshuttle_pid) + if sys.platform == 'win32': + sig = signal.CTRL_C_EVENT + else: + sig = signal.SIGINT + os.kill(sshuttle_pid, sig) -# Isolate function that needs to be replaced for tests -def _setup_daemon_unix(): +def _setup_daemon_for_unix_like(): if not is_admin_user(): - raise Fatal('You must be root (or enable su/sudo) to set the firewall') + raise Fatal('You must have root privileges (or enable su/sudo) to set the firewall') # don't disappear if our controlling terminal or stdout/stderr # disappears; we still have to clean up. @@ -115,7 +118,7 @@ def _setup_daemon_unix(): return sys.stdin, sys.stdout -def _setup_daemon_windows(): +def _setup_daemon_for_windows(): if not is_admin_user(): raise Fatal('You must be administrator to set the firewall') @@ -128,7 +131,7 @@ def _setup_daemon_windows(): debug3('Using shared socket for communicating with sshuttle client process') socket_share_data_b64 = line[len(socket_share_data_prefix):] socket_share_data = base64.b64decode(socket_share_data_b64) - sock = socket.fromshare(socket_share_data) + sock = socket.fromshare(socket_share_data) # type: socket.socket sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) sock.close() @@ -140,10 +143,11 @@ def _setup_daemon_windows(): return sys.stdin, sys.stdout +# Isolate function that needs to be replaced for tests if sys.platform == 'win32': - setup_daemon = _setup_daemon_windows + setup_daemon = _setup_daemon_for_windows else: - setup_daemon = _setup_daemon_unix + setup_daemon = _setup_daemon_for_unix_like # Note that we're sorting in a very particular order: @@ -226,10 +230,9 @@ def main(method_name, syslog): try: line = stdin.readline(128) if not line: - # parent probably exited - return - except IOError as e: - # On windows, this ConnectionResetError is thrown when parent process closes it's socket pair end + return # parent probably exited + except ConnectionResetError as e: + # On windows, ConnectionResetError is thrown when parent process closes it's socket pair end debug3('read from stdin failed: %s' % (e,)) return @@ -343,13 +346,13 @@ def main(method_name, syslog): except NotImplementedError: pass - if sys.platform != 'win32': + if sys.platform == 'linux': flush_systemd_dns_cache() try: stdout.write('STARTED\n') stdout.flush() - except IOError as e: + except IOError as e: # the parent process probably died debug3('write to stdout failed: %s' % (e,)) return @@ -410,7 +413,7 @@ def main(method_name, syslog): except Exception: debug2('An error occurred, ignoring it.') - if sys.platform != 'win32': + if sys.platform == 'linux': try: flush_systemd_dns_cache() except Exception: diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 979c13129..6ad857db3 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -3,6 +3,10 @@ import errno import os + +if sys.platform != "win32": + import fcntl + logprefix = '' verbose = 0 @@ -14,10 +18,10 @@ def b(s): def log(s): global logprefix try: - try: - sys.stdout.flush() - except (IOError, ValueError): - pass + sys.stdout.flush() + except IOError: + pass + try: # Put newline at end of string if line doesn't have one. if not s.endswith("\n"): s = s+"\n" @@ -234,4 +238,19 @@ def is_admin_user(): except Exception: return False + # TODO(nom3ad): for sys.platform == 'linux', support capabilities check for non-root users. (CAP_NET_ADMIN might be enough?) return os.getuid() == 0 + + +def set_non_blocking_io(fd): + if sys.platform != "win32": + try: + os.set_blocking(fd, False) + except AttributeError: + # python < 3.5 + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + flags |= os.O_NONBLOCK + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + else: + _sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) + _sock.setblocking(False) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index bb52edd2e..a654e70b4 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -46,6 +46,7 @@ def set_firewall(self, firewall): @staticmethod def get_supported_features(): result = Features() + result.loopback_port = True result.ipv4 = True result.ipv6 = False result.udp = False diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index da8c6bcc5..beb2e1546 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -350,6 +350,7 @@ def restore_firewall(self, port, family, udp, user): def get_supported_features(self): result = super(Method, self).get_supported_features() + result.loopback_port = False result.user = False result.dns = False result.ipv6 = False @@ -444,7 +445,7 @@ def _ingress_divert(self, ready_cb): if not ip_filters: raise Fatal("At least ipv4 or ipv6 address is expected") filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" - debug2(f"[INGRESS] {filter=}") + debug1(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter) as w: ready_cb() for pkt in w: diff --git a/sshuttle/options.py b/sshuttle/options.py index 93468ee5e..ac5a96da0 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -235,9 +235,14 @@ def convert_arg_line_to_args(self, arg_line): """ ) +if sys.platform == 'win32': + method_choices = ["auto", "windivert"] +else: + method_choices = ["auto", "nat", "tproxy", "pf", "ipfw"] + parser.add_argument( "--method", - choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"] if sys.platform != 'win32' else ["auto", "windivert"], + choices=method_choices, metavar="TYPE", default="auto", help=""" diff --git a/sshuttle/server.py b/sshuttle/server.py index 5aff9086e..867c041d8 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -281,7 +281,7 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver, sys.stdout.flush() handlers = [] - mux = Mux(sys.stdin, sys.stdout) + mux = Mux(sys.stdin.buffer, sys.stdout.buffer) handlers.append(mux) debug1('auto-nets:' + str(auto_nets)) diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index ee5a47ebb..394216511 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options): pyscript = r""" import sys, os; verbosity=%d; - sys.stdin = os.fdopen(0, "rb"); - exec(compile(sys.stdin.read(%d), "assembler.py", "exec")); + stdin = os.fdopen(0, "rb"); + exec(compile(stdin.read(%d), "assembler.py", "exec")); sys.exit(98); """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) @@ -213,24 +213,26 @@ def preexec_fn(): s2.close() s1.close() - def get_serversock(): + def get_server_io(): os.close(pstdin) os.close(pstdout) - return s2 + return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0) else: - # In Windows python implementation it seems not possible to use sockets as subprocess stdio - # Also select.select() won't work on pipes. - # So we have to use both socketpair and pipes together along with reader/writer threads to - # stream data between them - # NOTE: Their can be a way to use sockets as stdio with some hacks. + # In Windows CPython, we can't use BSD sockets as subprocess stdio + # and select.select() used in ssnet.py won't work on Windows pipes. + # So we have to use both socketpair (for select.select) and pipes (for subprocess.Popen) together + # along with reader/writer threads to stream data between them + # NOTE: Their could be a better way. Need to investigate further on this. + # Either to use sockets as stdio for subprocess. Or to use pipes but with a select() alternative # https://stackoverflow.com/questions/4993119/redirect-io-of-process-to-windows-socket + (s1, s2) = socket.socketpair() pstdin = ssubprocess.PIPE pstdout = ssubprocess.PIPE preexec_fn = None - def get_serversock(): + def get_server_io(): import threading def stream_stdout_to_sock(): @@ -267,7 +269,7 @@ def stream_sock_to_stdin(): p = ssubprocess.Popen(argv, stdin=pstdin, stdout=pstdout, preexec_fn=preexec_fn, close_fds=close_fds, stderr=stderr, bufsize=0) - serversock = get_serversock() - serversock.sendall(content) - serversock.sendall(content2) - return p, serversock + rfile, wfile = get_server_io() + wfile.write(content) + wfile.write(content2) + return p, rfile, wfile diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index cb15f9fef..798e42a55 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -5,10 +5,7 @@ import select import os -if sys.platform != "win32": - import fcntl - -from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal +from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, set_non_blocking_io MAX_CHANNEL = 65535 LATENCY_BUFFER_SIZE = 32768 @@ -215,10 +212,7 @@ def uwrite(self, buf): return 0 # still connecting self.wsock.setblocking(False) try: - if sys.platform == 'win32': - return _nb_clean(self.wsock.send, buf) - else: - return _nb_clean(os.write, self.wsock.fileno(), buf) + return _nb_clean(self.wsock.send, buf) except OSError: _, e = sys.exc_info()[:2] if e.errno == errno.EPIPE: @@ -241,10 +235,7 @@ def uread(self): return self.rsock.setblocking(False) try: - if sys.platform == 'win32': - return _nb_clean(self.rsock.recv, 65536) - else: - return _nb_clean(os.read, self.rsock.fileno(), 65536) + return _nb_clean(self.rsock.recv, 65536) except OSError: _, e = sys.exc_info()[:2] self.seterr('uread: %s' % e) @@ -439,22 +430,9 @@ def got_packet(self, channel, cmd, data): callback(cmd, data) def flush(self): - if sys.platform != "win32": - try: - os.set_blocking(self.wfile.fileno(), False) - except AttributeError: - # python < 3.5 - flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL) - flags |= os.O_NONBLOCK - fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) - else: - self.wfile.raw._sock.setblocking(False) - + set_non_blocking_io(self.wfile.fileno()) if self.outbuf and self.outbuf[0]: - if sys.platform == 'win32': - wrote = _nb_clean(self.wfile.raw._sock.send, self.outbuf[0]) - else: - wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) + wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0]))) if wrote: self.outbuf[0] = self.outbuf[0][wrote:] @@ -462,24 +440,11 @@ def flush(self): self.outbuf[0:1] = [] def fill(self): - if sys.platform != "win32": - try: - os.set_blocking(self.rfile.fileno(), False) - except AttributeError: - # python < 3.5 - flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL) - flags |= os.O_NONBLOCK - fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) - else: - self.rfile.raw._sock.setblocking(False) - + set_non_blocking_io(self.rfile.fileno()) try: # If LATENCY_BUFFER_SIZE is inappropriately large, we will # get a MemoryError here. Read no more than 1MiB. - if sys.platform == 'win32': - read = _nb_clean(self.rfile.raw._sock.recv, min(1048576, LATENCY_BUFFER_SIZE)) - else: - read = _nb_clean(os.read, self.rfile.fileno(), min(1048576, LATENCY_BUFFER_SIZE)) + read = _nb_clean(self.rfile.read, min(1048576, LATENCY_BUFFER_SIZE)) except OSError: _, e = sys.exc_info()[:2] raise Fatal('other end: %r' % e) From 4a84ad3be60a4e09f0298cf10c8af44f5655d737 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 00:24:31 +0530 Subject: [PATCH 186/275] fix windows CRLF issue on stdin/stdout --- sshuttle/client.py | 4 +- sshuttle/firewall.py | 64 ++- sshuttle/methods/windivert.py | 958 +++++++++++++++++----------------- sshuttle/ssh.py | 2 +- tests/client/test_firewall.py | 6 +- 5 files changed, 520 insertions(+), 514 deletions(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index e2179f441..232620ac7 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -307,7 +307,7 @@ def get_pfile(): def get_pfile(): if can_use_stdio: - self.p.stdin.write(b'STDIO:\n') + self.p.stdin.write(b'COM_STDIO:\n') self.p.stdin.flush() class RWPair: @@ -334,7 +334,7 @@ def close(self): socket_share_data = s1.share(self.p.pid) s1.close() socket_share_data_b64 = base64.b64encode(socket_share_data) - self.p.stdin.write(b'SOCKETSHARE:' + socket_share_data_b64 + b'\n') + self.p.stdin.write(b'COM_SOCKETSHARE:' + socket_share_data_b64 + b'\n') self.p.stdin.flush() return s2.makefile('rwb') try: diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 44276b9f3..3e3bb64fb 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -84,7 +84,7 @@ def firewall_exit(signum, frame): # the typical exit process as described above. global sshuttle_pid if sshuttle_pid: - debug1("Relaying interupt signal to sshuttle process %d\n" % sshuttle_pid) + debug1("Relaying interupt signal to sshuttle process %d" % sshuttle_pid) if sys.platform == 'win32': sig = signal.CTRL_C_EVENT else: @@ -115,7 +115,7 @@ def _setup_daemon_for_unix_like(): # setsid() fails if sudo is configured with the use_pty option. pass - return sys.stdin, sys.stdout + return sys.stdin.buffer, sys.stdout.buffer def _setup_daemon_for_windows(): @@ -125,9 +125,9 @@ def _setup_daemon_for_windows(): signal.signal(signal.SIGTERM, firewall_exit) signal.signal(signal.SIGINT, firewall_exit) - socket_share_data_prefix = 'SOCKETSHARE:' - line = sys.stdin.readline().strip() - if line.startswith('SOCKETSHARE:'): + socket_share_data_prefix = b'COM_SOCKETSHARE:' + line = sys.stdin.buffer.readline().strip() + if line.startswith(socket_share_data_prefix): debug3('Using shared socket for communicating with sshuttle client process') socket_share_data_b64 = line[len(socket_share_data_prefix):] socket_share_data = base64.b64decode(socket_share_data_b64) @@ -135,12 +135,12 @@ def _setup_daemon_for_windows(): sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) sock.close() - elif line.startswith("STDIO:"): + elif line.startswith(b"COM_STDIO:"): debug3('Using inherited stdio for communicating with sshuttle client process') else: raise Fatal("Unexpected stdin: " + line) - return sys.stdin, sys.stdout + return sys.stdin.buffer, sys.stdout.buffer # Isolate function that needs to be replaced for tests @@ -221,33 +221,43 @@ def main(method_name, syslog): "PATH." % method_name) debug1('ready method name %s.' % method.name) - stdout.write('READY %s\n' % method.name) + stdout.write(('READY %s\n' % method.name).encode('ASCII')) stdout.flush() + + def _read_next_string_line(): + try: + line = stdin.readline(128) + if not line: + return # parent probably exited + return line.decode('ASCII').strip() + except IOError as e: + # On windows, ConnectionResetError is thrown when parent process closes it's socket pair end + debug3('read from stdin failed: %s' % (e,)) + return # we wait until we get some input before creating the rules. That way, # sshuttle can launch us as early as possible (and get sudo password # authentication as early in the startup process as possible). try: - line = stdin.readline(128) + line = _read_next_string_line() if not line: return # parent probably exited - except ConnectionResetError as e: + except IOError as e: # On windows, ConnectionResetError is thrown when parent process closes it's socket pair end debug3('read from stdin failed: %s' % (e,)) return subnets = [] - if line != 'ROUTES\n': + if line != 'ROUTES': raise Fatal('expected ROUTES but got %r' % line) while 1: - line = stdin.readline(128) + line = _read_next_string_line() if not line: raise Fatal('expected route but got %r' % line) - elif line.startswith("NSLIST\n"): + elif line.startswith("NSLIST"): break try: - (family, width, exclude, ip, fport, lport) = \ - line.strip().split(',', 5) + (family, width, exclude, ip, fport, lport) = line.split(',', 5) except Exception: raise Fatal('expected route or NSLIST but got %r' % line) subnets.append(( @@ -260,16 +270,16 @@ def main(method_name, syslog): debug2('Got subnets: %r' % subnets) nslist = [] - if line != 'NSLIST\n': + if line != 'NSLIST': raise Fatal('expected NSLIST but got %r' % line) while 1: - line = stdin.readline(128) + line = _read_next_string_line() if not line: raise Fatal('expected nslist but got %r' % line) elif line.startswith("PORTS "): break try: - (family, ip) = line.strip().split(',', 1) + (family, ip) = line.split(',', 1) except Exception: raise Fatal('expected nslist or PORTS but got %r' % line) nslist.append((int(family), ip)) @@ -299,15 +309,13 @@ def main(method_name, syslog): debug2('Got ports: %d,%d,%d,%d' % (port_v6, port_v4, dnsport_v6, dnsport_v4)) - line = stdin.readline(128) - if not line: - raise Fatal('expected GO but got %r' % line) - elif not line.startswith("GO "): + line = _read_next_string_line() + if not line or not line.startswith("GO "): raise Fatal('expected GO but got %r' % line) _, _, args = line.partition(" ") global sshuttle_pid - udp, user, group, tmark, sshuttle_pid = args.strip().split(" ", 4) + udp, user, group, tmark, sshuttle_pid = args.split(" ", 4) udp = bool(int(udp)) sshuttle_pid = int(sshuttle_pid) if user == '-': @@ -350,7 +358,7 @@ def main(method_name, syslog): flush_systemd_dns_cache() try: - stdout.write('STARTED\n') + stdout.write(b'STARTED\n') stdout.flush() except IOError as e: # the parent process probably died debug3('write to stdout failed: %s' % (e,)) @@ -360,13 +368,11 @@ def main(method_name, syslog): # to stay running so that we don't need a *second* password # authentication at shutdown time - that cleanup is important! while 1: - try: - line = stdin.readline(128) - except IOError as e: - debug3('read from stdin failed: %s' % (e,)) + line = _read_next_string_line() + if not line: return if line.startswith('HOST '): - (name, ip) = line[5:].strip().split(',', 1) + (name, ip) = line[5:].split(',', 1) hostmap[name] = ip debug2('setting up /etc/hosts.') rewrite_etc_hosts(hostmap, port_v6 or port_v4) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index beb2e1546..dc3b1696c 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -1,479 +1,479 @@ -import os -import sys -import ipaddress -import threading -from collections import namedtuple -import socket -import subprocess -import re -from multiprocessing import shared_memory -import struct -from functools import wraps -from enum import IntEnum -import time -import traceback - - -from sshuttle.methods import BaseMethod -from sshuttle.helpers import debug3, log, debug1, debug2, Fatal - -try: - # https://reqrypt.org/windivert-doc.html#divert_iphdr - import pydivert -except ImportError: - raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert") - - -ConnectionTuple = namedtuple( - "ConnectionTuple", - ["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", "state"], -) - - -WINDIVERT_MAX_CONNECTIONS = 10_000 - - -class IPProtocol(IntEnum): - TCP = socket.IPPROTO_TCP - UDP = socket.IPPROTO_UDP - - @property - def filter(self): - return "tcp" if self == IPProtocol.TCP else "udp" - - -class IPFamily(IntEnum): - IPv4 = socket.AF_INET - IPv6 = socket.AF_INET6 - - @property - def filter(self): - return "ip" if self == socket.AF_INET else "ipv6" - - @property - def version(self): - return 4 if self == socket.AF_INET else 6 - - @property - def loopback_addr(self): - return "127.0.0.1" if self == socket.AF_INET else "::1" - - -class ConnState(IntEnum): - TCP_SYN_SENT = 11 # SYN sent - TCP_ESTABLISHED = 12 # SYN+ACK received - TCP_FIN_WAIT_1 = 91 # FIN sent - TCP_CLOSE_WAIT = 92 # FIN received - - @staticmethod - def can_timeout(state): - return state in (ConnState.TCP_SYN_SENT, ConnState.TCP_FIN_WAIT_1, ConnState.TCP_CLOSE_WAIT) - - -def repr_pkt(p): - r = f"{p.direction.name} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" - if p.tcp: - t = p.tcp - r += f" {len(t.payload)}B (" - r += "+".join( - f.upper() for f in ("fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr", "ns") if getattr(t, f) - ) - r += f") SEQ#{t.seq_num}" - if t.ack: - r += f" ACK#{t.ack_num}" - r += f" WZ={t.window_size}" - else: - r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}" - return f"" - - -def synchronized_method(lock): - def decorator(method): - @wraps(method) - def wrapped(self, *args, **kwargs): - with getattr(self, lock): - return method(self, *args, **kwargs) - - return wrapped - - return decorator - - -class ConnTrack: - - _instance = None - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = object.__new__(cls) - return cls._instance - raise RuntimeError("ConnTrack can not be instantiated multiple times") - - def __init__(self, name, max_connections=0) -> None: - self.struct_full_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B"))) - self.struct_src_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H"))) - self.struct_state_tuple = struct.Struct(">" + "".join(("L", "B"))) - - try: - self.max_connections = max_connections - self.shm_list = shared_memory.ShareableList( - [bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name - ) - self.is_owner = True - self.next_slot = 0 - self.used_slots = set() - self.rlock = threading.RLock() - except FileExistsError: - self.is_owner = False - self.shm_list = shared_memory.ShareableList(name=name) - self.max_connections = len(self.shm_list) - - debug2( - f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} " - f"shm_size={self.shm_list.shm.size}B" - ) - - @synchronized_method("rlock") - def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): - if not self.is_owner: - raise RuntimeError("Only owner can mutate ConnTrack") - if len(self.used_slots) >= self.max_connections: - raise RuntimeError(f"No slot available in ConnTrack {len(self.used_slots)}/{self.max_connections}") - - if self.get(proto, src_addr, src_port): - return - - for _ in range(self.max_connections): - if self.next_slot not in self.used_slots: - break - self.next_slot = (self.next_slot + 1) % self.max_connections - else: - raise RuntimeError("No slot available in ConnTrack") # should not be here - - src_addr = ipaddress.ip_address(src_addr) - dst_addr = ipaddress.ip_address(dst_addr) - assert src_addr.version == dst_addr.version - ip_version = src_addr.version - state_epoch = int(time.time()) - entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state) - packed = self.struct_full_tuple.pack(*entry) - self.shm_list[self.next_slot] = packed - self.used_slots.add(self.next_slot) - proto = IPProtocol(proto) - debug3( - f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to " - f"slot={self.next_slot} | #ActiveConn={len(self.used_slots)}" - ) - - @synchronized_method("rlock") - def update(self, proto, src_addr, src_port, state): - if not self.is_owner: - raise RuntimeError("Only owner can mutate ConnTrack") - src_addr = ipaddress.ip_address(src_addr) - packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) - for i in self.used_slots: - if self.shm_list[i].startswith(packed): - state_epoch = int(time.time()) - self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) - debug3( - f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | " - f"#ActiveConn={len(self.used_slots)}" - ) - return self._unpack(self.shm_list[i]) - else: - debug3( - f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | " - f"#ActiveConn={len(self.used_slots)}" - ) - - @synchronized_method("rlock") - def remove(self, proto, src_addr, src_port): - if not self.is_owner: - raise RuntimeError("Only owner can mutate ConnTrack") - src_addr = ipaddress.ip_address(src_addr) - packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) - for i in self.used_slots: - if self.shm_list[i].startswith(packed): - conn = self._unpack(self.shm_list[i]) - self.shm_list[i] = b"" - self.used_slots.remove(i) - debug3( - f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | " - f"#ActiveConn={len(self.used_slots)}" - ) - return conn - else: - debug3( - f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove |" - f" #ActiveConn={len(self.used_slots)}" - ) - - def get(self, proto, src_addr, src_port): - src_addr = ipaddress.ip_address(src_addr) - packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) - for entry in self.shm_list: - if entry and entry.startswith(packed): - return self._unpack(entry) - - @synchronized_method("rlock") - def gc(self, connection_timeout_sec=15): - now = int(time.time()) - n = 0 - for i in tuple(self.used_slots): - state_packed = self.shm_list[i][-5:] - (state_epoch, state) = self.struct_state_tuple.unpack(state_packed) - if (now - state_epoch) < connection_timeout_sec: - continue - if ConnState.can_timeout(state): - conn = self._unpack(self.shm_list[i]) - self.shm_list[i] = b"" - self.used_slots.remove(i) - n += 1 - debug3( - f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name})" - f" from slot={i} | #ActiveConn={len(self.used_slots)}" - ) - debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}") - - def _unpack(self, packed): - ( - proto, - ip_version, - src_addr_packed, - src_port, - dst_addr_packed, - dst_port, - state_epoch, - state, - ) = self.struct_full_tuple.unpack(packed) - dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) - src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) - return ConnectionTuple( - IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) - ) - - def __iter__(self): - def conn_iter(): - for i in self.used_slots: - yield self._unpack(self.shm_list[i]) - - return conn_iter() - - def __repr__(self): - return f"" - - -class Method(BaseMethod): - - network_config = {} - proxy_port = None - proxy_addr = {IPFamily.IPv4: None, IPFamily.IPv6: None} - - def __init__(self, name): - super().__init__(name) - - def _get_local_proxy_listen_addr(self, port, family): - proto = "TCPv6" if family.version == 6 else "TCP" - for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): - try: - _, local_addr, _, state, *_ = re.split(r"\s+", line.strip()) - except ValueError: - continue - port_suffix = ":" + str(port) - if state == "LISTENING" and local_addr.endswith(port_suffix): - return ipaddress.ip_address(local_addr[:-len(port_suffix)].strip("[]")) - raise Fatal("Could not find listening address for {}/{}".format(port, proto)) - - def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): - log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") - - if nslist or user or udp: - raise NotImplementedError() - - family = IPFamily(family) - - # using loopback proxy address never worked. - # >>> self.proxy_addr[family] = family.loopback_addr - # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) - # As a workaround we use another interface ip instead. - - local_addr = self._get_local_proxy_listen_addr(port, family) - for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): - if addr.is_loopback or addr.version != family.version: - continue - if local_addr.is_unspecified or local_addr == addr: - debug2("Found non loopback address to connect to proxy: " + str(addr)) - self.proxy_addr[family] = str(addr) - break - else: - raise Fatal("Windivert method requires proxy to listen on non loopback address") - - self.proxy_port = port - - subnet_addresses = [] - for (_, mask, exclude, network_addr, fport, lport) in subnets: - if exclude: - continue - assert fport == 0, "custom port range not supported" - assert lport == 0, "custom port range not supported" - subnet_addresses.append("%s/%s" % (network_addr, mask)) - - self.network_config[family] = { - "subnets": subnet_addresses, - "nslist": nslist, - } - - def wait_for_firewall_ready(self): - debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") - self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) - methods = (self._egress_divert, self._ingress_divert, self._connection_gc) - ready_events = [] - for fn in methods: - ev = threading.Event() - ready_events.append(ev) - - def _target(): - try: - fn(ev.set) - except Exception: - debug2(f"thread {fn.__name__} exiting due to: " + traceback.format_exc()) - sys.stdin.close() # this will exist main thread - sys.stdout.close() - - threading.Thread(name=fn.__name__, target=_target, daemon=True).start() - for ev in ready_events: - if not ev.wait(5): # at most 5 sec - raise Fatal("timeout in wait_for_firewall_ready()") - - def restore_firewall(self, port, family, udp, user): - pass - - def get_supported_features(self): - result = super(Method, self).get_supported_features() - result.loopback_port = False - result.user = False - result.dns = False - result.ipv6 = False - return result - - def get_tcp_dstip(self, sock): - if not hasattr(self, "conntrack"): - self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}") - - src_addr, src_port = sock.getpeername() - c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port) - if not c: - return (src_addr, src_port) - return (c.dst_addr, c.dst_port) - - def is_supported(self): - if sys.platform == "win32": - return True - return False - - def _egress_divert(self, ready_cb): - proto = IPProtocol.TCP - filter = f"outbound and {proto.filter}" - - # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: - family_filters = [] - for af, c in self.network_config.items(): - subnet_filters = [] - for cidr in c["subnets"]: - ip_network = ipaddress.ip_network(cidr) - first_ip = ip_network.network_address - last_ip = ip_network.broadcast_address - subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") - family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") - - filter = f"{filter} and ({' or '.join(family_filters)})" - - debug1(f"[OUTBOUND] {filter=}") - with pydivert.WinDivert(filter) as w: - ready_cb() - proxy_port = self.proxy_port - proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] - proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] - for pkt in w: - debug3(">>> " + repr_pkt(pkt)) - if pkt.tcp.syn and not pkt.tcp.ack: - # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) - self.conntrack.add( - socket.IPPROTO_TCP, - pkt.src_addr, - pkt.src_port, - pkt.dst_addr, - pkt.dst_port, - ConnState.TCP_SYN_SENT, - ) - if pkt.tcp.fin: - # FIN sent (start of graceful close our side, and we wait for ACK) - self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1) - if pkt.tcp.rst: - # RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply) - self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) - - # DNAT - if pkt.ipv4 and proxy_addr_ipv4: - pkt.dst_addr = proxy_addr_ipv4 - if pkt.ipv6 and proxy_addr_ipv6: - pkt.dst_addr = proxy_addr_ipv6 - pkt.tcp.dst_port = proxy_port - - # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well - # by setting src_addr to loopback address. - # Otherwise injecting packet will be ignored by Windows network stack - # as they packet has to cross public to private address space. - # See: https://github.com/basil00/Divert/issues/82 - # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. - # >>> pkt.dst_addr = proxy_addr_ipv4 - - w.send(pkt, recalculate_checksum=True) - - def _ingress_divert(self, ready_cb): - proto = IPProtocol.TCP - direction = "inbound" # only when proxy address is not loopback address (Useful for testing) - ip_filters = [] - for addr in (ipaddress.ip_address(a) for a in self.proxy_addr.values() if a): - if addr.is_loopback: # Windivert treats all loopback traffic as outbound - direction = "outbound" - if addr.version == 4: - ip_filters.append(f"ip.SrcAddr=={addr}") - else: - # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this - ip_filters.append(f"ipv6.SrcAddr=={addr}") - if not ip_filters: - raise Fatal("At least ipv4 or ipv6 address is expected") - filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" - debug1(f"[INGRESS] {filter=}") - with pydivert.WinDivert(filter) as w: - ready_cb() - for pkt in w: - debug3("<<< " + repr_pkt(pkt)) - if pkt.tcp.syn and pkt.tcp.ack: - # SYN+ACK received (connection established) - conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) - elif pkt.tcp.rst: - # RST received - Abrupt connection teardown initiated by otherside. We don't expect anymore packets - conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) - # https://wiki.wireshark.org/TCP-4-times-close.md - elif pkt.tcp.fin and pkt.tcp.ack: - # FIN+ACK received (Passive close by otherside. We don't expect any more packets. Otherside expects an ACK) - conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) - elif pkt.tcp.fin: - # FIN received (Otherside initiated graceful close. We expects a final ACK for a FIN packet) - conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT) - else: - conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) - if not conn: - debug2("Unexpected packet: " + repr_pkt(pkt)) - continue - pkt.src_addr = conn.dst_addr - pkt.tcp.src_port = conn.dst_port - w.send(pkt, recalculate_checksum=True) - - def _connection_gc(self, ready_cb): - ready_cb() - while True: - time.sleep(5) - self.conntrack.gc() +import os +import sys +import ipaddress +import threading +from collections import namedtuple +import socket +import subprocess +import re +from multiprocessing import shared_memory +import struct +from functools import wraps +from enum import IntEnum +import time +import traceback + + +from sshuttle.methods import BaseMethod +from sshuttle.helpers import debug3, log, debug1, debug2, Fatal + +try: + # https://reqrypt.org/windivert-doc.html#divert_iphdr + import pydivert +except ImportError: + raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert") + + +ConnectionTuple = namedtuple( + "ConnectionTuple", + ["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", "state"], +) + + +WINDIVERT_MAX_CONNECTIONS = 10_000 + + +class IPProtocol(IntEnum): + TCP = socket.IPPROTO_TCP + UDP = socket.IPPROTO_UDP + + @property + def filter(self): + return "tcp" if self == IPProtocol.TCP else "udp" + + +class IPFamily(IntEnum): + IPv4 = socket.AF_INET + IPv6 = socket.AF_INET6 + + @property + def filter(self): + return "ip" if self == socket.AF_INET else "ipv6" + + @property + def version(self): + return 4 if self == socket.AF_INET else 6 + + @property + def loopback_addr(self): + return "127.0.0.1" if self == socket.AF_INET else "::1" + + +class ConnState(IntEnum): + TCP_SYN_SENT = 11 # SYN sent + TCP_ESTABLISHED = 12 # SYN+ACK received + TCP_FIN_WAIT_1 = 91 # FIN sent + TCP_CLOSE_WAIT = 92 # FIN received + + @staticmethod + def can_timeout(state): + return state in (ConnState.TCP_SYN_SENT, ConnState.TCP_FIN_WAIT_1, ConnState.TCP_CLOSE_WAIT) + + +def repr_pkt(p): + r = f"{p.direction.name} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" + if p.tcp: + t = p.tcp + r += f" {len(t.payload)}B (" + r += "+".join( + f.upper() for f in ("fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr", "ns") if getattr(t, f) + ) + r += f") SEQ#{t.seq_num}" + if t.ack: + r += f" ACK#{t.ack_num}" + r += f" WZ={t.window_size}" + else: + r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}" + return f"" + + +def synchronized_method(lock): + def decorator(method): + @wraps(method) + def wrapped(self, *args, **kwargs): + with getattr(self, lock): + return method(self, *args, **kwargs) + + return wrapped + + return decorator + + +class ConnTrack: + + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = object.__new__(cls) + return cls._instance + raise RuntimeError("ConnTrack can not be instantiated multiple times") + + def __init__(self, name, max_connections=0) -> None: + self.struct_full_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B"))) + self.struct_src_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H"))) + self.struct_state_tuple = struct.Struct(">" + "".join(("L", "B"))) + + try: + self.max_connections = max_connections + self.shm_list = shared_memory.ShareableList( + [bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name + ) + self.is_owner = True + self.next_slot = 0 + self.used_slots = set() + self.rlock = threading.RLock() + except FileExistsError: + self.is_owner = False + self.shm_list = shared_memory.ShareableList(name=name) + self.max_connections = len(self.shm_list) + + debug2( + f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} " + f"shm_size={self.shm_list.shm.size}B" + ) + + @synchronized_method("rlock") + def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + if len(self.used_slots) >= self.max_connections: + raise RuntimeError(f"No slot available in ConnTrack {len(self.used_slots)}/{self.max_connections}") + + if self.get(proto, src_addr, src_port): + return + + for _ in range(self.max_connections): + if self.next_slot not in self.used_slots: + break + self.next_slot = (self.next_slot + 1) % self.max_connections + else: + raise RuntimeError("No slot available in ConnTrack") # should not be here + + src_addr = ipaddress.ip_address(src_addr) + dst_addr = ipaddress.ip_address(dst_addr) + assert src_addr.version == dst_addr.version + ip_version = src_addr.version + state_epoch = int(time.time()) + entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state) + packed = self.struct_full_tuple.pack(*entry) + self.shm_list[self.next_slot] = packed + self.used_slots.add(self.next_slot) + proto = IPProtocol(proto) + debug3( + f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to " + f"slot={self.next_slot} | #ActiveConn={len(self.used_slots)}" + ) + + @synchronized_method("rlock") + def update(self, proto, src_addr, src_port, state): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for i in self.used_slots: + if self.shm_list[i].startswith(packed): + state_epoch = int(time.time()) + self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state) + debug3( + f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | " + f"#ActiveConn={len(self.used_slots)}" + ) + return self._unpack(self.shm_list[i]) + else: + debug3( + f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | " + f"#ActiveConn={len(self.used_slots)}" + ) + + @synchronized_method("rlock") + def remove(self, proto, src_addr, src_port): + if not self.is_owner: + raise RuntimeError("Only owner can mutate ConnTrack") + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for i in self.used_slots: + if self.shm_list[i].startswith(packed): + conn = self._unpack(self.shm_list[i]) + self.shm_list[i] = b"" + self.used_slots.remove(i) + debug3( + f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | " + f"#ActiveConn={len(self.used_slots)}" + ) + return conn + else: + debug3( + f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove |" + f" #ActiveConn={len(self.used_slots)}" + ) + + def get(self, proto, src_addr, src_port): + src_addr = ipaddress.ip_address(src_addr) + packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) + for entry in self.shm_list: + if entry and entry.startswith(packed): + return self._unpack(entry) + + @synchronized_method("rlock") + def gc(self, connection_timeout_sec=15): + now = int(time.time()) + n = 0 + for i in tuple(self.used_slots): + state_packed = self.shm_list[i][-5:] + (state_epoch, state) = self.struct_state_tuple.unpack(state_packed) + if (now - state_epoch) < connection_timeout_sec: + continue + if ConnState.can_timeout(state): + conn = self._unpack(self.shm_list[i]) + self.shm_list[i] = b"" + self.used_slots.remove(i) + n += 1 + debug3( + f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name})" + f" from slot={i} | #ActiveConn={len(self.used_slots)}" + ) + debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}") + + def _unpack(self, packed): + ( + proto, + ip_version, + src_addr_packed, + src_port, + dst_addr_packed, + dst_port, + state_epoch, + state, + ) = self.struct_full_tuple.unpack(packed) + dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) + src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) + return ConnectionTuple( + IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) + ) + + def __iter__(self): + def conn_iter(): + for i in self.used_slots: + yield self._unpack(self.shm_list[i]) + + return conn_iter() + + def __repr__(self): + return f"" + + +class Method(BaseMethod): + + network_config = {} + proxy_port = None + proxy_addr = {IPFamily.IPv4: None, IPFamily.IPv6: None} + + def __init__(self, name): + super().__init__(name) + + def _get_local_proxy_listen_addr(self, port, family): + proto = "TCPv6" if family.version == 6 else "TCP" + for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): + try: + _, local_addr, _, state, *_ = re.split(r"\s+", line.strip()) + except ValueError: + continue + port_suffix = ":" + str(port) + if state == "LISTENING" and local_addr.endswith(port_suffix): + return ipaddress.ip_address(local_addr[:-len(port_suffix)].strip("[]")) + raise Fatal("Could not find listening address for {}/{}".format(port, proto)) + + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, group, tmark): + log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + + if nslist or user or udp: + raise NotImplementedError() + + family = IPFamily(family) + + # using loopback proxy address never worked. + # >>> self.proxy_addr[family] = family.loopback_addr + # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) + # As a workaround we use another interface ip instead. + + local_addr = self._get_local_proxy_listen_addr(port, family) + for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): + if addr.is_loopback or addr.version != family.version: + continue + if local_addr.is_unspecified or local_addr == addr: + debug2("Found non loopback address to connect to proxy: " + str(addr)) + self.proxy_addr[family] = str(addr) + break + else: + raise Fatal("Windivert method requires proxy to listen on non loopback address") + + self.proxy_port = port + + subnet_addresses = [] + for (_, mask, exclude, network_addr, fport, lport) in subnets: + if exclude: + continue + assert fport == 0, "custom port range not supported" + assert lport == 0, "custom port range not supported" + subnet_addresses.append("%s/%s" % (network_addr, mask)) + + self.network_config[family] = { + "subnets": subnet_addresses, + "nslist": nslist, + } + + def wait_for_firewall_ready(self): + debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") + self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) + methods = (self._egress_divert, self._ingress_divert, self._connection_gc) + ready_events = [] + for fn in methods: + ev = threading.Event() + ready_events.append(ev) + + def _target(): + try: + fn(ev.set) + except Exception: + debug2(f"thread {fn.__name__} exiting due to: " + traceback.format_exc()) + sys.stdin.close() # this will exist main thread + sys.stdout.close() + + threading.Thread(name=fn.__name__, target=_target, daemon=True).start() + for ev in ready_events: + if not ev.wait(5): # at most 5 sec + raise Fatal("timeout in wait_for_firewall_ready()") + + def restore_firewall(self, port, family, udp, user, group): + pass + + def get_supported_features(self): + result = super(Method, self).get_supported_features() + result.loopback_port = False + result.user = False + result.dns = False + result.ipv6 = False + return result + + def get_tcp_dstip(self, sock): + if not hasattr(self, "conntrack"): + self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}") + + src_addr, src_port = sock.getpeername() + c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port) + if not c: + return (src_addr, src_port) + return (c.dst_addr, c.dst_port) + + def is_supported(self): + if sys.platform == "win32": + return True + return False + + def _egress_divert(self, ready_cb): + proto = IPProtocol.TCP + filter = f"outbound and {proto.filter}" + + # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: + family_filters = [] + for af, c in self.network_config.items(): + subnet_filters = [] + for cidr in c["subnets"]: + ip_network = ipaddress.ip_network(cidr) + first_ip = ip_network.network_address + last_ip = ip_network.broadcast_address + subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") + family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") + + filter = f"{filter} and ({' or '.join(family_filters)})" + + debug1(f"[OUTBOUND] {filter=}") + with pydivert.WinDivert(filter) as w: + ready_cb() + proxy_port = self.proxy_port + proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] + proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] + for pkt in w: + debug3(">>> " + repr_pkt(pkt)) + if pkt.tcp.syn and not pkt.tcp.ack: + # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) + self.conntrack.add( + socket.IPPROTO_TCP, + pkt.src_addr, + pkt.src_port, + pkt.dst_addr, + pkt.dst_port, + ConnState.TCP_SYN_SENT, + ) + if pkt.tcp.fin: + # FIN sent (start of graceful close our side, and we wait for ACK) + self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1) + if pkt.tcp.rst: + # RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply) + self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) + + # DNAT + if pkt.ipv4 and proxy_addr_ipv4: + pkt.dst_addr = proxy_addr_ipv4 + if pkt.ipv6 and proxy_addr_ipv6: + pkt.dst_addr = proxy_addr_ipv6 + pkt.tcp.dst_port = proxy_port + + # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well + # by setting src_addr to loopback address. + # Otherwise injecting packet will be ignored by Windows network stack + # as they packet has to cross public to private address space. + # See: https://github.com/basil00/Divert/issues/82 + # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. + # >>> pkt.dst_addr = proxy_addr_ipv4 + + w.send(pkt, recalculate_checksum=True) + + def _ingress_divert(self, ready_cb): + proto = IPProtocol.TCP + direction = "inbound" # only when proxy address is not loopback address (Useful for testing) + ip_filters = [] + for addr in (ipaddress.ip_address(a) for a in self.proxy_addr.values() if a): + if addr.is_loopback: # Windivert treats all loopback traffic as outbound + direction = "outbound" + if addr.version == 4: + ip_filters.append(f"ip.SrcAddr=={addr}") + else: + # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this + ip_filters.append(f"ipv6.SrcAddr=={addr}") + if not ip_filters: + raise Fatal("At least ipv4 or ipv6 address is expected") + filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" + debug1(f"[INGRESS] {filter=}") + with pydivert.WinDivert(filter) as w: + ready_cb() + for pkt in w: + debug3("<<< " + repr_pkt(pkt)) + if pkt.tcp.syn and pkt.tcp.ack: + # SYN+ACK received (connection established) + conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) + elif pkt.tcp.rst: + # RST received - Abrupt connection teardown initiated by otherside. We don't expect anymore packets + conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) + # https://wiki.wireshark.org/TCP-4-times-close.md + elif pkt.tcp.fin and pkt.tcp.ack: + # FIN+ACK received (Passive close by otherside. We don't expect any more packets. Otherside expects an ACK) + conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) + elif pkt.tcp.fin: + # FIN received (Otherside initiated graceful close. We expects a final ACK for a FIN packet) + conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT) + else: + conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) + if not conn: + debug2("Unexpected packet: " + repr_pkt(pkt)) + continue + pkt.src_addr = conn.dst_addr + pkt.tcp.src_port = conn.dst_port + w.send(pkt, recalculate_checksum=True) + + def _connection_gc(self, ready_cb): + ready_cb() + while True: + time.sleep(5) + self.conntrack.gc() diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 394216511..7b494d201 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -260,7 +260,7 @@ def stream_sock_to_stdin(): threading.Thread(target=stream_stdout_to_sock, name='stream_stdout_to_sock', daemon=True).start() threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() - return s2 + return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0) # https://stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows close_fds = False if sys.platform == 'win32' else True diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index e714c81c4..02a73f7cd 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -10,7 +10,7 @@ def setup_daemon(): - stdin = io.StringIO(u"""ROUTES + stdin = io.BytesIO(u"""ROUTES {inet},24,0,1.2.3.0,8000,9000 {inet},32,1,1.2.3.66,8080,8080 {inet6},64,0,2404:6800:4004:80c::,0,0 @@ -127,9 +127,9 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): ] assert stdout.mock_calls == [ - call.write('READY test\n'), + call.write(b'READY test\n'), call.flush(), - call.write('STARTED\n'), + call.write(b'STARTED\n'), call.flush() ] assert mock_setup_daemon.mock_calls == [call()] From d4d0fa945d50606360aa7c5f026a0f190b026c68 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 01:31:24 +0530 Subject: [PATCH 187/275] fix: bad file descriptor error in windows, fix pytest errors --- hack/exec-tool | 4 ++-- hack/run-checks | 9 +++++++++ sshuttle/firewall.py | 1 - sshuttle/ssnet.py | 3 ++- tests/client/test_firewall.py | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) create mode 100755 hack/run-checks diff --git a/hack/exec-tool b/hack/exec-tool index 47b84cae0..28a0882ed 100755 --- a/hack/exec-tool +++ b/hack/exec-tool @@ -24,7 +24,7 @@ ping) ;; iperf3) port=5001 - with_set_x exec iperf3 --client "$ip" --port=$port --connect-timeout=$connect_timeout_sec "$@" + with_set_x exec iperf3 --client "$ip" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "$@" ;; curl) port=8080 @@ -32,6 +32,6 @@ curl) ;; ab) port=8080 - with_set_x exec ab -n 100 -c 20 -s $connect_timeout_sec "$@" "http://$ip:$port/" + with_set_x exec ab -n 100 -c 20 -s $connect_timeout_sec "$@" "http://$ip:$port/" ;; esac diff --git a/hack/run-checks b/hack/run-checks new file mode 100755 index 000000000..72bead234 --- /dev/null +++ b/hack/run-checks @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/.." + +export PYTHONPATH=. + +set -x +python -m pytest . +python -m flake8 . \ No newline at end of file diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 3e3bb64fb..9532ab054 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -224,7 +224,6 @@ def main(method_name, syslog): stdout.write(('READY %s\n' % method.name).encode('ASCII')) stdout.flush() - def _read_next_string_line(): try: line = stdin.readline(128) diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index 798e42a55..6110b4ca1 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -432,7 +432,8 @@ def got_packet(self, channel, cmd, data): def flush(self): set_non_blocking_io(self.wfile.fileno()) if self.outbuf and self.outbuf[0]: - wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) + wrote = _nb_clean(self.wfile.write, self.outbuf[0]) + self.wfile.flush() debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0]))) if wrote: self.outbuf[0] = self.outbuf[0][wrote:] diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 02a73f7cd..f9d4fdb79 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -21,7 +21,7 @@ def setup_daemon(): PORTS 1024,1025,1026,1027 GO 1 - - 0x01 12345 HOST 1.2.3.3,existing -""".format(inet=AF_INET, inet6=AF_INET6)) +""".format(inet=AF_INET, inet6=AF_INET6).encode('ASCII')) stdout = Mock() return stdin, stdout From db9ec36fac2bb7bb052fefce6bfff37f37048c00 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 09:46:22 +0530 Subject: [PATCH 188/275] better test-bed scripts --- hack/README.md | 11 +++++++++++ hack/compose.yml | 4 ---- hack/exec-sshuttle | 4 ++-- hack/exec-tool | 9 +++++++-- hack/run-benchmark | 12 +++++------- hack/run-checks | 2 +- hack/setup.service | 7 +++++-- hack/test-bed | 22 +++++++++++++++++----- 8 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 hack/README.md diff --git a/hack/README.md b/hack/README.md new file mode 100644 index 000000000..bd4056822 --- /dev/null +++ b/hack/README.md @@ -0,0 +1,11 @@ +# Container based test bed for sshuttle + +```bash +test-bed up -d # start containers + +exec-sshuttle node-1 # start sshuttle to connect to node-1 + +exec-tool curl node-1 # curl to nginx instance running on node1 via IP that is only reachable via sshuttle +exec-tool iperf3 node-1 # measure throughput to node-1 + +``` diff --git a/hack/compose.yml b/hack/compose.yml index e008e6d28..cd086e850 100644 --- a/hack/compose.yml +++ b/hack/compose.yml @@ -5,8 +5,6 @@ services: image: ghcr.io/sshuttle/sshuttle-testbed container_name: sshuttle-testbed-node-1 hostname: node-1 - ports: - - 22001:2222 cap_add: - "NET_ADMIN" environment: @@ -15,8 +13,6 @@ services: image: ghcr.io/sshuttle/sshuttle-testbed container_name: sshuttle-testbed-node-2 hostname: node-2 - ports: - - 22002:2222 cap_add: - "NET_ADMIN" environment: diff --git a/hack/exec-sshuttle b/hack/exec-sshuttle index 5d5f692d7..b76a90f5a 100755 --- a/hack/exec-sshuttle +++ b/hack/exec-sshuttle @@ -9,9 +9,9 @@ if [[ ! $node =~ [1-9]+ ]]; then fi shift -port="2200$node" +port="2222" subnet_args="-N" -host=localhost +host=$("$(dirname "$0")/test-bed" get-ip "$node") user="test:test" if ! command -v sshpass >/dev/null; then diff --git a/hack/exec-tool b/hack/exec-tool index 28a0882ed..e70de9cdb 100755 --- a/hack/exec-tool +++ b/hack/exec-tool @@ -2,10 +2,11 @@ set -e tool=${1?:"tool argument missing. should be one of iperf3,ping,curl,ab"} -node=${2?:"node argument missing. should be '1' , '2' etc"} +node=${2?:"node argument missing. should be 'node-1' , 'node-2' etc"} shift 2 -ip="10.55.$node.77" +index=${node#node-} +ip="10.55.$index.77" connect_timeout_sec=3 function with_set_x() { @@ -34,4 +35,8 @@ ab) port=8080 with_set_x exec ab -n 100 -c 20 -s $connect_timeout_sec "$@" "http://$ip:$port/" ;; +*) + echo "Unknown tool: $tool" >&2 + exit 2 + ;; esac diff --git a/hack/run-benchmark b/hack/run-benchmark index 2ba4386a3..07a858b44 100755 --- a/hack/run-benchmark +++ b/hack/run-benchmark @@ -5,14 +5,15 @@ cd "$(dirname "$0")" function with_set_x() { set -x "$@" - { ec=$?; set +x;return $ec; } 2>/dev/null + { + ec=$? + set +x + return $ec + } 2>/dev/null } - ./test-bed up -d - - benchmark() { local sshuttle_bin="${1?:}" echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin ========" @@ -29,12 +30,9 @@ benchmark() { wait $sshuttle_pid || true } - if [[ "$1" ]]; then benchmark "$1" else benchmark "${SSHUTTLE_BIN:-/bin/sshuttle}" benchmark dev fi - - diff --git a/hack/run-checks b/hack/run-checks index 72bead234..92b606ad0 100755 --- a/hack/run-checks +++ b/hack/run-checks @@ -6,4 +6,4 @@ export PYTHONPATH=. set -x python -m pytest . -python -m flake8 . \ No newline at end of file +python -m flake8 . diff --git a/hack/setup.service b/hack/setup.service index 8a6b0b740..4e36124f9 100755 --- a/hack/setup.service +++ b/hack/setup.service @@ -8,10 +8,13 @@ echo -e ">>> Setting up $(hostname) | id: $(id) | $(python --version) \nip: $(ip function with_set_x() { set -x "$@" - { ec=$?; set +x;return $ec; } 2>/dev/null + { + ec=$? + set +x + return $ec + } 2>/dev/null } - iface="$(ip route | awk '/default/ { print $5 }')" default_gw="$(ip route | awk '/default/ { print $3 }')" for addr in ${IP_ADDRESSES//,/ }; do diff --git a/hack/test-bed b/hack/test-bed index dddb04fe3..cb0504317 100755 --- a/hack/test-bed +++ b/hack/test-bed @@ -2,7 +2,6 @@ set -e cd "$(dirname "$0")" - if [[ -z $1 || $1 = -* ]]; then set -- up "$@" fi @@ -10,7 +9,11 @@ fi function with_set_x() { set -x "$@" - { ec=$?; set +x;return $ec; } 2>/dev/null + { + ec=$? + set +x + return $ec + } 2>/dev/null } function build() { @@ -23,8 +26,17 @@ function compose() { with_set_x docker compose "$@" } +function get-ip() { + local container_name=sshuttle-testbed-"$1" + docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name" +} -if [[ $* = *--build* ]]; then - build +if [[ $1 == get-ip ]]; then + shift + get-ip "$@" +else + if [[ $* = *--build* ]]; then + build + fi + compose "$@" fi -compose "$@" From 371258991f6449ded802e95e1d42a2c64b6fe9f2 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:03:24 +0530 Subject: [PATCH 189/275] Update exec-sshuttle script and related files --- hack/README.md | 2 ++ hack/exec-sshuttle | 74 ++++++++++++++++++++++++++++++++++++---------- hack/exec-tool | 20 +++++++++---- hack/run-benchmark | 7 +++-- hack/run-checks | 2 +- 5 files changed, 80 insertions(+), 25 deletions(-) diff --git a/hack/README.md b/hack/README.md index bd4056822..e2be7ead4 100644 --- a/hack/README.md +++ b/hack/README.md @@ -3,6 +3,8 @@ ```bash test-bed up -d # start containers +exec-sshuttle [--copy-id] [--shuttle-bin=/path/to/sshttle] [sshuttle-args...] + exec-sshuttle node-1 # start sshuttle to connect to node-1 exec-tool curl node-1 # curl to nginx instance running on node1 via IP that is only reachable via sshuttle diff --git a/hack/exec-sshuttle b/hack/exec-sshuttle index b76a90f5a..4061908cd 100755 --- a/hack/exec-sshuttle +++ b/hack/exec-sshuttle @@ -1,29 +1,73 @@ #!/usr/bin/env bash set -e -node=$1 +function with_set_x() { + set -x + "$@" + { + ec=$? + set +x + return $ec + } 2>/dev/null +} + + +ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' +args=() +while [[ $# -gt 0 ]]; do + arg=$1 + shift + case "$arg" in + -v) + ssh_cmd+='-v' + ;; + --copy-id) + ssh_copy_id=true + continue + ;; + --sshuttle-bin=*) + sshuttle_bin="${arg#*=}" + continue + ;; + -*) + ;; + *) + if [[ -z "$node" ]]; then + node=$arg + continue + fi + ;; + esac + args+=("$arg") +done -if [[ ! $node =~ [1-9]+ ]]; then - echo "node argument missing. should be '1' , '2' etc" - exit 2 -fi -shift port="2222" -subnet_args="-N" -host=$("$(dirname "$0")/test-bed" get-ip "$node") user="test:test" +if [[ $node == node-* ]]; then + host=$("$(dirname "$0")/test-bed" get-ip "$node") + index=${node#node-} + args+=("10.55.$index.0/24") +else + host=$node +fi + +if [[ "${args[$(( ${#args[@]} - 1 ))]}" != *.* && "${args[$(( ${#args[@]} - 1 ))]}" != *:* ]]; then + echo "No subnet specified. Using -N" >&2 + args+=('-N') +fi + if ! command -v sshpass >/dev/null; then - echo "sshpass is not found. You have to manually enter ssh password: 'test'" >&2 - user="test" + echo "sshpass is not found. You might have to manually enter ssh password: 'test'" >&2 + user=${user%:*} fi -ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' -if [[ "$*" =~ -v ]]; then - ssh_cmd+='-v' +if [[ $ssh_copy_id == true ]]; then + echo "Trying to make it passwordless" >&2 + with_set_x ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host" fi -SSHUTTLE_BIN=${SSHUTTLE_BIN:-"$(dirname "$0")/../run"} +sshuttle_bin=${sshuttle_bin:-"$(dirname "$0")/../run"} set -x -exec "${SSHUTTLE_BIN}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "$@" $subnet_args +exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}" diff --git a/hack/exec-tool b/hack/exec-tool index e70de9cdb..22f67bfc9 100755 --- a/hack/exec-tool +++ b/hack/exec-tool @@ -5,8 +5,13 @@ tool=${1?:"tool argument missing. should be one of iperf3,ping,curl,ab"} node=${2?:"node argument missing. should be 'node-1' , 'node-2' etc"} shift 2 -index=${node#node-} -ip="10.55.$index.77" +if [[ $node == node-* ]]; then + index=${node#node-} + host="10.55.$index.77" +else + host=$node +fi + connect_timeout_sec=3 function with_set_x() { @@ -21,19 +26,22 @@ function with_set_x() { case "$tool" in ping) - with_set_x exec ping -W $connect_timeout_sec "$@" "$ip" + with_set_x exec ping -W $connect_timeout_sec "$@" "$host" ;; iperf3) port=5001 - with_set_x exec iperf3 --client "$ip" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "$@" + with_set_x exec iperf3 --client "$host" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "$@" ;; curl) port=8080 - with_set_x exec curl "http://$ip:$port/" -v --connect-timeout $connect_timeout_sec "$@" + with_set_x exec curl "http://$host:$port/" -v --connect-timeout $connect_timeout_sec "$@" ;; ab) port=8080 - with_set_x exec ab -n 100 -c 20 -s $connect_timeout_sec "$@" "http://$ip:$port/" + if [[ " $*" != *" -n "* && " $*" != *" -c "* ]]; then + set -- -n 500 -c 50 "$@" + fi + with_set_x exec ab -s $connect_timeout_sec "$@" "http://$host:$port/" ;; *) echo "Unknown tool: $tool" >&2 diff --git a/hack/run-benchmark b/hack/run-benchmark index 07a858b44..a17d63403 100755 --- a/hack/run-benchmark +++ b/hack/run-benchmark @@ -16,11 +16,12 @@ function with_set_x() { benchmark() { local sshuttle_bin="${1?:}" + local node="${2:-'node-1'}" echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin ========" if [[ "$sshuttle_bin" == dev ]]; then sshuttle_bin="../run" fi - SSHUTTLE_BIN=$sshuttle_bin ./exec-sshuttle 1 --listen 55771 & + ./exec-sshuttle "$node" --sshuttle-bin="$sshuttle_bin" --listen 55771 & sshuttle_pid=$! trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT while ! nc -z localhost 55771; do sleep 0.1; done @@ -33,6 +34,6 @@ benchmark() { if [[ "$1" ]]; then benchmark "$1" else - benchmark "${SSHUTTLE_BIN:-/bin/sshuttle}" - benchmark dev + benchmark "${SSHUTTLE_BIN:-/bin/sshuttle}" node-1 + benchmark dev node-1 fi diff --git a/hack/run-checks b/hack/run-checks index 92b606ad0..cdd518389 100755 --- a/hack/run-checks +++ b/hack/run-checks @@ -5,5 +5,5 @@ cd "$(dirname "$0")/.." export PYTHONPATH=. set -x +python -m flake8 sshuttle tests python -m pytest . -python -m flake8 . From e19fc0132410b1f3e7805e4ea257b52bf2688435 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:53:20 +0530 Subject: [PATCH 190/275] !improved windrivert throughput --- hack/exec-sshuttle | 9 +++++- hack/run-benchmark | 5 +-- sshuttle/__main__.py | 5 ++- sshuttle/client.py | 59 +++++++++++++---------------------- sshuttle/firewall.py | 15 +++------ sshuttle/helpers.py | 21 +++++++++++++ sshuttle/methods/windivert.py | 40 +++++++++++++----------- sshuttle/ssh.py | 2 +- 8 files changed, 81 insertions(+), 75 deletions(-) diff --git a/hack/exec-sshuttle b/hack/exec-sshuttle index 4061908cd..0a82cfbf5 100755 --- a/hack/exec-sshuttle +++ b/hack/exec-sshuttle @@ -68,6 +68,13 @@ if [[ $ssh_copy_id == true ]]; then with_set_x ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host" fi -sshuttle_bin=${sshuttle_bin:-"$(dirname "$0")/../run"} +if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then + cd "$(dirname "$0")/.." + export PYTHONPATH="." + sshuttle_bin="./run" +fi + set -x + exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}" + diff --git a/hack/run-benchmark b/hack/run-benchmark index a17d63403..72b2ca02c 100755 --- a/hack/run-benchmark +++ b/hack/run-benchmark @@ -18,9 +18,6 @@ benchmark() { local sshuttle_bin="${1?:}" local node="${2:-'node-1'}" echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin ========" - if [[ "$sshuttle_bin" == dev ]]; then - sshuttle_bin="../run" - fi ./exec-sshuttle "$node" --sshuttle-bin="$sshuttle_bin" --listen 55771 & sshuttle_pid=$! trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT @@ -34,6 +31,6 @@ benchmark() { if [[ "$1" ]]; then benchmark "$1" else - benchmark "${SSHUTTLE_BIN:-/bin/sshuttle}" node-1 + benchmark "${SSHUTTLE_BIN:-sshuttle}" node-1 benchmark dev node-1 fi diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index c7566790d..3b4209334 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -3,9 +3,8 @@ import os from sshuttle.cmdline import main from sshuttle.helpers import debug3 -from sshuttle import __version__ -debug3("Starting cmd %r (pid:%s) | sshuttle: %s | Python: %s" % (sys.argv, os.getpid(), __version__, sys.version)) +debug3("Start: (pid=%s, ppid=%s) %r" % (os.getpid(), os.getppid(), sys.argv)) exit_code = main() -debug3("Exiting cmd %r (pid:%s) with code %s" % (sys.argv, os.getpid(), exit_code,)) +debug3("Exit: (pid=%s, ppid=%s, code=%s) cmd %r" % (os.getpid(), os.getppid(), exit_code, sys.argv)) sys.exit(exit_code) diff --git a/sshuttle/client.py b/sshuttle/client.py index 232620ac7..c837b24cb 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -5,6 +5,7 @@ import subprocess as ssubprocess import os import sys +import base64 import platform import sshuttle.helpers as helpers @@ -14,7 +15,7 @@ import sshuttle.sdnotify as sdnotify from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ - resolvconf_nameservers, which, is_admin_user + resolvconf_nameservers, which, is_admin_user, RWPair from sshuttle.methods import get_method, Features from sshuttle import __version__ try: @@ -294,48 +295,32 @@ def get_pfile(): return s2.makefile('rwb') else: - # In windows, if client/firewall processes is running as admin user, stdio can be used for communication. - # But if firewall process is run with elevated mode, access to stdio is lost. - # So we have to use a socketpair (as in unix). - # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows + # In Windows CPython, BSD sockets are not supported as subprocess stdio. + # if client (and firewall) processes is running as admin user, pipe based stdio can be used for communication. + # But if firewall process is spwaned in elevated mode by non-admin client process, access to stdio is lost. + # To work around this, we can use a socketpair. + # But socket need to be "shared" to child process as it can't be directly set as stdio. can_use_stdio = is_admin_user() - pstdout = ssubprocess.PIPE if can_use_stdio else None - pstdin = ssubprocess.PIPE + preexec_fn = None penv = os.environ.copy() - penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) + if can_use_stdio: + pstdout = ssubprocess.PIPE + pstdin = ssubprocess.PIPE - def get_pfile(): - if can_use_stdio: - self.p.stdin.write(b'COM_STDIO:\n') - self.p.stdin.flush() - - class RWPair: - def __init__(self, r, w): - self.r = r - self.w = w - self.read = r.read - self.readline = r.readline - self.write = w.write - self.flush = w.flush - - def close(self): - for f in self.r, self.w: - try: - f.close() - except Exception: - pass + def get_pfile(): return RWPair(self.p.stdout, self.p.stdin) - # import io - # return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) - else: - import base64 - (s1, s2) = socket.socketpair() - socket_share_data = s1.share(self.p.pid) + penv['SSHUTTLE_FW_COM_CHANNEL'] = 'stdio' + else: + pstdout = None + pstdin = None + (s1, s2) = socket.socketpair() + socket_share_data = s1.share(self.p.pid) + socket_share_data_b64 = base64.b64encode(socket_share_data) + penv['SSHUTTLE_FW_COM_CHANNEL'] = socket_share_data_b64 + + def get_pfile(): s1.close() - socket_share_data_b64 = base64.b64encode(socket_share_data) - self.p.stdin.write(b'COM_SOCKETSHARE:' + socket_share_data_b64 + b'\n') - self.p.stdin.flush() return s2.makefile('rwb') try: debug1("Starting firewall manager with command: %r" % argv) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 9532ab054..2ec9e2538 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -125,21 +125,16 @@ def _setup_daemon_for_windows(): signal.signal(signal.SIGTERM, firewall_exit) signal.signal(signal.SIGINT, firewall_exit) - socket_share_data_prefix = b'COM_SOCKETSHARE:' - line = sys.stdin.buffer.readline().strip() - if line.startswith(socket_share_data_prefix): + com_chan = os.environ.get('SSHUTTLE_FW_COM_CHANNEL') + if com_chan == 'stdio': + debug3('Using inherited stdio for communicating with sshuttle client process') + else: debug3('Using shared socket for communicating with sshuttle client process') - socket_share_data_b64 = line[len(socket_share_data_prefix):] - socket_share_data = base64.b64decode(socket_share_data_b64) + socket_share_data = base64.b64decode(com_chan) sock = socket.fromshare(socket_share_data) # type: socket.socket sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0)) sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True) sock.close() - elif line.startswith(b"COM_STDIO:"): - debug3('Using inherited stdio for communicating with sshuttle client process') - else: - raise Fatal("Unexpected stdin: " + line) - return sys.stdin.buffer, sys.stdout.buffer diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 6ad857db3..c682150a0 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -15,6 +15,10 @@ def b(s): return s.encode("ASCII") +def get_verbose_level(): + return verbose + + def log(s): global logprefix try: @@ -254,3 +258,20 @@ def set_non_blocking_io(fd): else: _sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) _sock.setblocking(False) + + +class RWPair: + def __init__(self, r, w): + self.r = r + self.w = w + self.read = r.read + self.readline = r.readline + self.write = w.write + self.flush = w.flush + + def close(self): + for f in self.r, self.w: + try: + f.close() + except Exception: + pass diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index dc3b1696c..b17a31645 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -1,6 +1,6 @@ import os import sys -import ipaddress +from ipaddress import ip_address, ip_network import threading from collections import namedtuple import socket @@ -15,7 +15,7 @@ from sshuttle.methods import BaseMethod -from sshuttle.helpers import debug3, log, debug1, debug2, Fatal +from sshuttle.helpers import debug3, log, debug1, debug2, get_verbose_level, Fatal try: # https://reqrypt.org/windivert-doc.html#divert_iphdr @@ -30,7 +30,7 @@ ) -WINDIVERT_MAX_CONNECTIONS = 10_000 +WINDIVERT_MAX_CONNECTIONS = int(os.environ.get('WINDIVERT_MAX_CONNECTIONS', 1024)) class IPProtocol(IntEnum): @@ -150,8 +150,8 @@ def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): else: raise RuntimeError("No slot available in ConnTrack") # should not be here - src_addr = ipaddress.ip_address(src_addr) - dst_addr = ipaddress.ip_address(dst_addr) + src_addr = ip_address(src_addr) + dst_addr = ip_address(dst_addr) assert src_addr.version == dst_addr.version ip_version = src_addr.version state_epoch = int(time.time()) @@ -169,7 +169,7 @@ def add(self, proto, src_addr, src_port, dst_addr, dst_port, state): def update(self, proto, src_addr, src_port, state): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") - src_addr = ipaddress.ip_address(src_addr) + src_addr = ip_address(src_addr) packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) for i in self.used_slots: if self.shm_list[i].startswith(packed): @@ -190,7 +190,7 @@ def update(self, proto, src_addr, src_port, state): def remove(self, proto, src_addr, src_port): if not self.is_owner: raise RuntimeError("Only owner can mutate ConnTrack") - src_addr = ipaddress.ip_address(src_addr) + src_addr = ip_address(src_addr) packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) for i in self.used_slots: if self.shm_list[i].startswith(packed): @@ -209,7 +209,7 @@ def remove(self, proto, src_addr, src_port): ) def get(self, proto, src_addr, src_port): - src_addr = ipaddress.ip_address(src_addr) + src_addr = ip_address(src_addr) packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port) for entry in self.shm_list: if entry and entry.startswith(packed): @@ -246,8 +246,8 @@ def _unpack(self, packed): state_epoch, state, ) = self.struct_full_tuple.unpack(packed) - dst_addr = str(ipaddress.ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) - src_addr = str(ipaddress.ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) + dst_addr = str(ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) + src_addr = str(ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) return ConnectionTuple( IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) ) @@ -281,7 +281,7 @@ def _get_local_proxy_listen_addr(self, port, family): continue port_suffix = ":" + str(port) if state == "LISTENING" and local_addr.endswith(port_suffix): - return ipaddress.ip_address(local_addr[:-len(port_suffix)].strip("[]")) + return ip_address(local_addr[:-len(port_suffix)].strip("[]")) raise Fatal("Could not find listening address for {}/{}".format(port, proto)) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, group, tmark): @@ -298,7 +298,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, grou # As a workaround we use another interface ip instead. local_addr = self._get_local_proxy_listen_addr(port, family) - for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): + for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): if addr.is_loopback or addr.version != family.version: continue if local_addr.is_unspecified or local_addr == addr: @@ -380,9 +380,9 @@ def _egress_divert(self, ready_cb): for af, c in self.network_config.items(): subnet_filters = [] for cidr in c["subnets"]: - ip_network = ipaddress.ip_network(cidr) - first_ip = ip_network.network_address - last_ip = ip_network.broadcast_address + ip_net = ip_network(cidr) + first_ip = ip_net.network_address + last_ip = ip_net.broadcast_address subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") @@ -394,8 +394,9 @@ def _egress_divert(self, ready_cb): proxy_port = self.proxy_port proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] + verbose = get_verbose_level() for pkt in w: - debug3(">>> " + repr_pkt(pkt)) + verbose >= 3 and debug3(">>> " + repr_pkt(pkt)) if pkt.tcp.syn and not pkt.tcp.ack: # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) self.conntrack.add( @@ -434,7 +435,7 @@ def _ingress_divert(self, ready_cb): proto = IPProtocol.TCP direction = "inbound" # only when proxy address is not loopback address (Useful for testing) ip_filters = [] - for addr in (ipaddress.ip_address(a) for a in self.proxy_addr.values() if a): + for addr in (ip_address(a) for a in self.proxy_addr.values() if a): if addr.is_loopback: # Windivert treats all loopback traffic as outbound direction = "outbound" if addr.version == 4: @@ -448,8 +449,9 @@ def _ingress_divert(self, ready_cb): debug1(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter) as w: ready_cb() + verbose = get_verbose_level() for pkt in w: - debug3("<<< " + repr_pkt(pkt)) + verbose >= 3 and debug3("<<< " + repr_pkt(pkt)) if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK received (connection established) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) @@ -466,7 +468,7 @@ def _ingress_divert(self, ready_cb): else: conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: - debug2("Unexpected packet: " + repr_pkt(pkt)) + verbose >= 2 and debug2("Unexpected packet: " + repr_pkt(pkt)) continue pkt.src_addr = conn.dst_addr pkt.tcp.src_port = conn.dst_port diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 7b494d201..08ea5df04 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -218,7 +218,7 @@ def get_server_io(): os.close(pstdout) return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0) else: - # In Windows CPython, we can't use BSD sockets as subprocess stdio + # In Windows CPython, BSD sockets are not supported as subprocess stdio # and select.select() used in ssnet.py won't work on Windows pipes. # So we have to use both socketpair (for select.select) and pipes (for subprocess.Popen) together # along with reader/writer threads to stream data between them From 8fa15c3ca85573439d1d70f6765265acb908e622 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:53:49 +0530 Subject: [PATCH 191/275] support windivert > 2.0 --- sshuttle/helpers.py | 4 ++-- sshuttle/methods/windivert.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index c682150a0..519216dcc 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -23,7 +23,7 @@ def log(s): global logprefix try: sys.stdout.flush() - except IOError: + except (IOError, ValueError): # ValueError ~ I/O operation on closed file pass try: # Put newline at end of string if line doesn't have one. @@ -36,7 +36,7 @@ def log(s): sys.stderr.write(prefix + line + "\n") prefix = " " sys.stderr.flush() - except IOError: + except (IOError, ValueError): # ValueError ~ I/O operation on closed file # this could happen if stderr gets forcibly disconnected, eg. because # our tty closes. That sucks, but it's no reason to abort the program. pass diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index b17a31645..8064ae5cf 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -71,7 +71,15 @@ def can_timeout(state): def repr_pkt(p): - r = f"{p.direction.name} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" + try: + direction = p.direction.name + if p.is_loopback: + direction += "/lo" + except AttributeError: # windiver > 2.0 + direction = 'OUT' if p.address.Outbound == 1 else 'IN' + if p.address.Loopback == 1: + direction += '/lo' + r = f"{direction} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}" if p.tcp: t = p.tcp r += f" {len(t.payload)}B (" @@ -389,7 +397,7 @@ def _egress_divert(self, ready_cb): filter = f"{filter} and ({' or '.join(family_filters)})" debug1(f"[OUTBOUND] {filter=}") - with pydivert.WinDivert(filter) as w: + with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: ready_cb() proxy_port = self.proxy_port proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] @@ -428,7 +436,6 @@ def _egress_divert(self, ready_cb): # See: https://github.com/basil00/Divert/issues/82 # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. # >>> pkt.dst_addr = proxy_addr_ipv4 - w.send(pkt, recalculate_checksum=True) def _ingress_divert(self, ready_cb): @@ -447,7 +454,7 @@ def _ingress_divert(self, ready_cb): raise Fatal("At least ipv4 or ipv6 address is expected") filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" debug1(f"[INGRESS] {filter=}") - with pydivert.WinDivert(filter) as w: + with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: ready_cb() verbose = get_verbose_level() for pkt in w: From dadfba488bc5d944b47727bd4daf4cb78f526a6b Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 21:13:30 +0530 Subject: [PATCH 192/275] better windivert filters --- sshuttle/methods/windivert.py | 81 +++++++++++++++++------------------ 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 8064ae5cf..b6f6a950d 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -19,6 +19,7 @@ try: # https://reqrypt.org/windivert-doc.html#divert_iphdr + # https://www.reqrypt.org/windivert-changelog.txt import pydivert except ImportError: raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert") @@ -275,12 +276,11 @@ class Method(BaseMethod): network_config = {} proxy_port = None - proxy_addr = {IPFamily.IPv4: None, IPFamily.IPv6: None} def __init__(self, name): super().__init__(name) - def _get_local_proxy_listen_addr(self, port, family): + def _get_bind_addresses_for_port(self, port, family): proto = "TCPv6" if family.version == 6 else "TCP" for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): try: @@ -292,31 +292,28 @@ def _get_local_proxy_listen_addr(self, port, family): return ip_address(local_addr[:-len(port_suffix)].strip("[]")) raise Fatal("Could not find listening address for {}/{}".format(port, proto)) - def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, group, tmark): - log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark): + log(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") if nslist or user or udp: raise NotImplementedError() family = IPFamily(family) - # using loopback proxy address never worked. - # >>> self.proxy_addr[family] = family.loopback_addr - # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) - # As a workaround we use another interface ip instead. - - local_addr = self._get_local_proxy_listen_addr(port, family) + proxy_ip = None + # using loopback only proxy binding won't work with windivert. + # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 https://github.com/basil00/Divert/issues/82) + # As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address) + local_addr = self._get_bind_addresses_for_port(proxy_port, family) for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): if addr.is_loopback or addr.version != family.version: continue if local_addr.is_unspecified or local_addr == addr: debug2("Found non loopback address to connect to proxy: " + str(addr)) - self.proxy_addr[family] = str(addr) + proxy_ip = str(addr) break else: - raise Fatal("Windivert method requires proxy to listen on non loopback address") - - self.proxy_port = port + raise Fatal("Windivert method requires proxy to listen on a non loopback address") subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: @@ -329,10 +326,11 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, grou self.network_config[family] = { "subnets": subnet_addresses, "nslist": nslist, + "proxy_addr": (proxy_ip, proxy_port) } def wait_for_firewall_ready(self): - debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") + debug2(f"network_config={self.network_config}") self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) methods = (self._egress_divert, self._ingress_divert, self._connection_gc) ready_events = [] @@ -380,6 +378,7 @@ def is_supported(self): return False def _egress_divert(self, ready_cb): + """divert outgoing packets to proxy""" proto = IPProtocol.TCP filter = f"outbound and {proto.filter}" @@ -391,20 +390,21 @@ def _egress_divert(self, ready_cb): ip_net = ip_network(cidr) first_ip = ip_net.network_address last_ip = ip_net.broadcast_address - subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") - family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") + subnet_filters.append(f"({af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip})") + proxy_ip, proxy_port = c["proxy_addr"] + proxy_guard_filter = f'({af.filter}.DstAddr!={proxy_ip} or tcp.DstPort!={proxy_port})' + family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) and {proxy_guard_filter}") filter = f"{filter} and ({' or '.join(family_filters)})" - debug1(f"[OUTBOUND] {filter=}") + debug1(f"[EGRESS] {filter=}") with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: ready_cb() - proxy_port = self.proxy_port - proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] - proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6] + proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"] if IPFamily.IPv4 in self.network_config else None + proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"] if IPFamily.IPv6 in self.network_config else None verbose = get_verbose_level() for pkt in w: - verbose >= 3 and debug3(">>> " + repr_pkt(pkt)) + verbose >= 3 and debug3("[EGRESS] " + repr_pkt(pkt)) if pkt.tcp.syn and not pkt.tcp.ack: # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) self.conntrack.add( @@ -423,11 +423,10 @@ def _egress_divert(self, ready_cb): self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) # DNAT - if pkt.ipv4 and proxy_addr_ipv4: - pkt.dst_addr = proxy_addr_ipv4 - if pkt.ipv6 and proxy_addr_ipv6: - pkt.dst_addr = proxy_addr_ipv6 - pkt.tcp.dst_port = proxy_port + if pkt.ipv4 and proxy_ipv4: + pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv4 + if pkt.ipv6 and proxy_ipv6: + pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv6 # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well # by setting src_addr to loopback address. @@ -435,30 +434,28 @@ def _egress_divert(self, ready_cb): # as they packet has to cross public to private address space. # See: https://github.com/basil00/Divert/issues/82 # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. - # >>> pkt.dst_addr = proxy_addr_ipv4 + # >>> pkt.dst_addr = proxy_ipv4 w.send(pkt, recalculate_checksum=True) def _ingress_divert(self, ready_cb): + """handles incoming packets from proxy""" proto = IPProtocol.TCP - direction = "inbound" # only when proxy address is not loopback address (Useful for testing) - ip_filters = [] - for addr in (ip_address(a) for a in self.proxy_addr.values() if a): - if addr.is_loopback: # Windivert treats all loopback traffic as outbound - direction = "outbound" - if addr.version == 4: - ip_filters.append(f"ip.SrcAddr=={addr}") - else: - # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this - ip_filters.append(f"ipv6.SrcAddr=={addr}") - if not ip_filters: - raise Fatal("At least ipv4 or ipv6 address is expected") - filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" + # Windivert treats all local process traffic as outbound, regardless of origin external/loopback iface + direction = "outbound" + proxy_addr_filters = [] + for af, c in self.network_config.items(): + proxy_ip, proxy_port = c["proxy_addr"] + # "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this + proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip} and tcp.SrcPort=={proxy_port}") + if not proxy_addr_filters: + raise Fatal("At least one ipv4 or ipv6 address is expected") + filter = f"{direction} and {proto.filter} and ({' or '.join(proxy_addr_filters)})" debug1(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: ready_cb() verbose = get_verbose_level() for pkt in w: - verbose >= 3 and debug3("<<< " + repr_pkt(pkt)) + verbose >= 3 and debug3("[INGRESS] " + repr_pkt(pkt)) if pkt.tcp.syn and pkt.tcp.ack: # SYN+ACK received (connection established) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) From 1885974f52bdef2cc1c1b6e5c1ea36af16343b86 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:13:06 +0530 Subject: [PATCH 193/275] refactor for future ipv6 support --- hack/compose.yml | 14 ++++++++-- hack/exec-sshuttle | 24 ++++++++++------- hack/exec-tool | 50 ++++++++++++++++++++++++++++------- hack/setup.service | 2 +- sshuttle/methods/windivert.py | 24 ++++++++++------- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/hack/compose.yml b/hack/compose.yml index cd086e850..5bdb4e539 100644 --- a/hack/compose.yml +++ b/hack/compose.yml @@ -8,7 +8,10 @@ services: cap_add: - "NET_ADMIN" environment: - - IP_ADDRESSES=10.55.1.77/24 + - ADD_IP_ADDRESSES=10.55.1.77/24 + networks: + default: + ipv6_address: 2001:0DB8::551 node-2: image: ghcr.io/sshuttle/sshuttle-testbed container_name: sshuttle-testbed-node-2 @@ -16,9 +19,16 @@ services: cap_add: - "NET_ADMIN" environment: - - IP_ADDRESSES=10.55.2.77/32 + - ADD_IP_ADDRESSES=10.55.2.77/32 + networks: + default: + ipv6_address: 2001:0DB8::552 networks: default: driver: bridge + enable_ipv6: true + ipam: + config: + - subnet: 2001:0DB8::/112 # internal: true \ No newline at end of file diff --git a/hack/exec-sshuttle b/hack/exec-sshuttle index 0a82cfbf5..3412342c2 100755 --- a/hack/exec-sshuttle +++ b/hack/exec-sshuttle @@ -11,7 +11,6 @@ function with_set_x() { } 2>/dev/null } - ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' args=() while [[ $# -gt 0 ]]; do @@ -24,13 +23,16 @@ while [[ $# -gt 0 ]]; do --copy-id) ssh_copy_id=true continue - ;; + ;; + -6) + ipv6_only=true + continue + ;; --sshuttle-bin=*) sshuttle_bin="${arg#*=}" continue - ;; - -*) ;; + -*) ;; *) if [[ -z "$node" ]]; then node=$arg @@ -41,19 +43,22 @@ while [[ $# -gt 0 ]]; do args+=("$arg") done - port="2222" user="test:test" if [[ $node == node-* ]]; then host=$("$(dirname "$0")/test-bed" get-ip "$node") index=${node#node-} - args+=("10.55.$index.0/24") + if [[ $ipv6_only == true ]]; then + args+=("2001:0DB8::/112") + else + args+=("10.55.$index.0/24") + fi else host=$node fi -if [[ "${args[$(( ${#args[@]} - 1 ))]}" != *.* && "${args[$(( ${#args[@]} - 1 ))]}" != *:* ]]; then +if [[ "${args[$((${#args[@]} - 1))]}" != *.* && "${args[$((${#args[@]} - 1))]}" != *:* ]]; then echo "No subnet specified. Using -N" >&2 args+=('-N') fi @@ -68,7 +73,7 @@ if [[ $ssh_copy_id == true ]]; then with_set_x ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host" fi -if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then +if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then cd "$(dirname "$0")/.." export PYTHONPATH="." sshuttle_bin="./run" @@ -76,5 +81,4 @@ fi set -x -exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}" - +exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}" diff --git a/hack/exec-tool b/hack/exec-tool index 22f67bfc9..fc4eedc59 100755 --- a/hack/exec-tool +++ b/hack/exec-tool @@ -1,13 +1,39 @@ #!/usr/bin/env bash set -e -tool=${1?:"tool argument missing. should be one of iperf3,ping,curl,ab"} -node=${2?:"node argument missing. should be 'node-1' , 'node-2' etc"} -shift 2 +args=() +while [[ $# -gt 0 ]]; do + arg=$1 + shift + case "$arg" in + -6) + ipv6_only=true + continue + ;; + -*) ;; + *) + if [[ -z $tool ]]; then + tool=$arg + continue + elif [[ -z $node ]]; then + node=$arg + continue + fi + ;; + esac + args+=("$arg") +done + +tool=${tool?:"tool argument missing. should be one of iperf3,ping,curl,ab"} +node=${node?:"node argument missing. should be 'node-1' , 'node-2' etc"} if [[ $node == node-* ]]; then index=${node#node-} - host="10.55.$index.77" + if [[ $ipv6_only == true ]]; then + host="2001:0DB8::55$index" + else + host="10.55.$index.77" + fi else host=$node fi @@ -26,22 +52,26 @@ function with_set_x() { case "$tool" in ping) - with_set_x exec ping -W $connect_timeout_sec "$@" "$host" + with_set_x exec ping -W $connect_timeout_sec "${args[@]}" "$host" ;; iperf3) port=5001 - with_set_x exec iperf3 --client "$host" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "$@" + with_set_x exec iperf3 --client "$host" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "${args[@]}" ;; curl) port=8080 - with_set_x exec curl "http://$host:$port/" -v --connect-timeout $connect_timeout_sec "$@" + if [[ $host = *:* ]]; then + host="[$host]" + args+=(--ipv6) + fi + with_set_x exec curl "http://$host:$port/" -v --connect-timeout $connect_timeout_sec "${args[@]}" ;; ab) port=8080 - if [[ " $*" != *" -n "* && " $*" != *" -c "* ]]; then - set -- -n 500 -c 50 "$@" + if [[ " ${args[*]}" != *" -n "* && " ${args[*]}" != *" -c "* ]]; then + args+=(-n 500 -c 50 "${args[@]}") fi - with_set_x exec ab -s $connect_timeout_sec "$@" "http://$host:$port/" + with_set_x exec ab -s $connect_timeout_sec "${args[@]}" "http://$host:$port/" ;; *) echo "Unknown tool: $tool" >&2 diff --git a/hack/setup.service b/hack/setup.service index 4e36124f9..fb4935394 100755 --- a/hack/setup.service +++ b/hack/setup.service @@ -17,7 +17,7 @@ function with_set_x() { iface="$(ip route | awk '/default/ { print $5 }')" default_gw="$(ip route | awk '/default/ { print $3 }')" -for addr in ${IP_ADDRESSES//,/ }; do +for addr in ${ADD_IP_ADDRESSES//,/ }; do echo ">>> Adding $addr to interface $iface" net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}') with_set_x ip addr add "$addr" dev "$iface" diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index b6f6a950d..389c3b72d 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -255,8 +255,8 @@ def _unpack(self, packed): state_epoch, state, ) = self.struct_full_tuple.unpack(packed) - dst_addr = str(ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4])) - src_addr = str(ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4])) + dst_addr = ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4]).exploded + src_addr = ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4]).exploded return ConnectionTuple( IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) ) @@ -306,14 +306,15 @@ def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user # As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address) local_addr = self._get_bind_addresses_for_port(proxy_port, family) for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): - if addr.is_loopback or addr.version != family.version: + if addr.version != family.version or addr.is_loopback or addr.is_link_local: continue if local_addr.is_unspecified or local_addr == addr: - debug2("Found non loopback address to connect to proxy: " + str(addr)) - proxy_ip = str(addr) + proxy_ip = addr.exploded + debug2("Found non loopback address to connect to proxy: " + proxy_ip) break else: - raise Fatal("Windivert method requires proxy to listen on a non loopback address") + raise Fatal("Windivert method requires proxy to be reachable by a non loopback address." + f"No addersss found for {family.name}") subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: @@ -388,15 +389,18 @@ def _egress_divert(self, ready_cb): subnet_filters = [] for cidr in c["subnets"]: ip_net = ip_network(cidr) - first_ip = ip_net.network_address - last_ip = ip_net.broadcast_address + first_ip = ip_net.network_address.exploded + last_ip = ip_net.broadcast_address.exploded subnet_filters.append(f"({af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip})") + if not subnet_filters: + continue proxy_ip, proxy_port = c["proxy_addr"] proxy_guard_filter = f'({af.filter}.DstAddr!={proxy_ip} or tcp.DstPort!={proxy_port})' family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) and {proxy_guard_filter}") + if not family_filters: + raise Fatal("At least one ipv4 or ipv6 subnet is expected") filter = f"{filter} and ({' or '.join(family_filters)})" - debug1(f"[EGRESS] {filter=}") with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: ready_cb() @@ -444,6 +448,8 @@ def _ingress_divert(self, ready_cb): direction = "outbound" proxy_addr_filters = [] for af, c in self.network_config.items(): + if not c["subnets"]: + continue proxy_ip, proxy_port = c["proxy_addr"] # "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip} and tcp.SrcPort=={proxy_port}") From cd2d69ac0825035ee499e17a45f89ca1a9b6257a Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:21:29 +0530 Subject: [PATCH 194/275] Bump version to 1.2.0 --- setup.cfg | 2 +- setup.py | 2 +- sshuttle/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 494952755..7f47b3dd1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.2 +current_version = 1.2.0 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 673eea80e..d51c5f6ed 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="sshuttle", - version='1.1.2', + version='1.2.0', url='https://github.com/sshuttle/sshuttle', author='Brian May', author_email='brian@linuxpenguins.xyz', diff --git a/sshuttle/version.py b/sshuttle/version.py index 706230230..af8fb55e1 100644 --- a/sshuttle/version.py +++ b/sshuttle/version.py @@ -1 +1 @@ -__version__ = version = '1.1.2' +__version__ = version = '1.2.0' From b0799f87527016eab92864562c30c330274f368c Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:41:02 +0530 Subject: [PATCH 195/275] Fix benchmarking script to use correct node parameter --- hack/run-benchmark | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/run-benchmark b/hack/run-benchmark index 72b2ca02c..94600bfb3 100755 --- a/hack/run-benchmark +++ b/hack/run-benchmark @@ -16,14 +16,14 @@ function with_set_x() { benchmark() { local sshuttle_bin="${1?:}" - local node="${2:-'node-1'}" + local node="${2:-"node-1"}" echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin ========" - ./exec-sshuttle "$node" --sshuttle-bin="$sshuttle_bin" --listen 55771 & + with_set_x ./exec-sshuttle "$node" --sshuttle-bin="$sshuttle_bin" --listen 55771 & sshuttle_pid=$! trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT while ! nc -z localhost 55771; do sleep 0.1; done sleep 1 - ./exec-tool iperf3 1 --time=4 + ./exec-tool iperf3 "$node" --time=4 with_set_x kill -15 $sshuttle_pid wait $sshuttle_pid || true } From 32fceefa764f81cdd0b2d99988bbc885fa2480c7 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:06:05 +0530 Subject: [PATCH 196/275] !fix: windows installed script execution --- sshuttle/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sshuttle/client.py b/sshuttle/client.py index c837b24cb..687942518 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -210,7 +210,10 @@ class FirewallClient: def __init__(self, method_name, sudo_pythonpath): self.auto_nets = [] - argvbase = ([sys.executable, sys.argv[0]] + + argv0 = sys.argv[0] + # if argv0 is a not python script, it shall be an executable. + # In windows it will be a .exe file and other platforms it will be a shebang script + argvbase = (([sys.executable, sys.argv[0]] if argv0.endswith('.py') else [argv0]) + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + ['--firewall']) From de8a19ce694d85a81d52628c79d88b0935a5455f Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:21:27 +0530 Subject: [PATCH 197/275] rename hacks to scripts --- {hack => scripts}/Containerfile | 0 {hack => scripts}/README.md | 2 +- {hack => scripts}/compose.yml | 0 {hack => scripts}/exec-sshuttle | 2 +- {hack => scripts}/exec-tool | 0 {hack => scripts}/run-benchmark | 0 {hack => scripts}/run-checks | 0 {hack => scripts}/setup.service | 0 {hack => scripts}/test-bed | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename {hack => scripts}/Containerfile (100%) rename {hack => scripts}/README.md (78%) rename {hack => scripts}/compose.yml (100%) rename {hack => scripts}/exec-sshuttle (98%) rename {hack => scripts}/exec-tool (100%) rename {hack => scripts}/run-benchmark (100%) rename {hack => scripts}/run-checks (100%) rename {hack => scripts}/setup.service (100%) rename {hack => scripts}/test-bed (100%) diff --git a/hack/Containerfile b/scripts/Containerfile similarity index 100% rename from hack/Containerfile rename to scripts/Containerfile diff --git a/hack/README.md b/scripts/README.md similarity index 78% rename from hack/README.md rename to scripts/README.md index e2be7ead4..f335d431a 100644 --- a/hack/README.md +++ b/scripts/README.md @@ -3,7 +3,7 @@ ```bash test-bed up -d # start containers -exec-sshuttle [--copy-id] [--shuttle-bin=/path/to/sshttle] [sshuttle-args...] +exec-sshuttle [--copy-id] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...] exec-sshuttle node-1 # start sshuttle to connect to node-1 diff --git a/hack/compose.yml b/scripts/compose.yml similarity index 100% rename from hack/compose.yml rename to scripts/compose.yml diff --git a/hack/exec-sshuttle b/scripts/exec-sshuttle similarity index 98% rename from hack/exec-sshuttle rename to scripts/exec-sshuttle index 3412342c2..a42f58330 100755 --- a/hack/exec-sshuttle +++ b/scripts/exec-sshuttle @@ -80,5 +80,5 @@ if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then fi set -x - +$sshuttle_bin --version exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}" diff --git a/hack/exec-tool b/scripts/exec-tool similarity index 100% rename from hack/exec-tool rename to scripts/exec-tool diff --git a/hack/run-benchmark b/scripts/run-benchmark similarity index 100% rename from hack/run-benchmark rename to scripts/run-benchmark diff --git a/hack/run-checks b/scripts/run-checks similarity index 100% rename from hack/run-checks rename to scripts/run-checks diff --git a/hack/setup.service b/scripts/setup.service similarity index 100% rename from hack/setup.service rename to scripts/setup.service diff --git a/hack/test-bed b/scripts/test-bed similarity index 100% rename from hack/test-bed rename to scripts/test-bed From 72060abbeffb32c28df74b0af0db7ad8f202cf60 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Sun, 7 Jan 2024 21:32:50 +0530 Subject: [PATCH 198/275] code cleanup and small refactoring --- scripts/exec-sshuttle | 2 +- sshuttle/client.py | 80 +++++++++++++++++------------------ sshuttle/helpers.py | 2 +- sshuttle/methods/__init__.py | 2 +- sshuttle/methods/windivert.py | 48 ++++++++++++--------- sshuttle/ssh.py | 2 +- 6 files changed, 73 insertions(+), 63 deletions(-) diff --git a/scripts/exec-sshuttle b/scripts/exec-sshuttle index a42f58330..660bed26f 100755 --- a/scripts/exec-sshuttle +++ b/scripts/exec-sshuttle @@ -58,7 +58,7 @@ else host=$node fi -if [[ "${args[$((${#args[@]} - 1))]}" != *.* && "${args[$((${#args[@]} - 1))]}" != *:* ]]; then +if [[ "${#args[@]}" -ne 0 && "${args[$((${#args[@]} - 1))]}" != *.* && "${args[$((${#args[@]} - 1))]}" != *:* ]]; then echo "No subnet specified. Using -N" >&2 args+=('-N') fi diff --git a/sshuttle/client.py b/sshuttle/client.py index 687942518..8271cb903 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -211,8 +211,8 @@ def __init__(self, method_name, sudo_pythonpath): self.auto_nets = [] argv0 = sys.argv[0] - # if argv0 is a not python script, it shall be an executable. - # In windows it will be a .exe file and other platforms it will be a shebang script + # argv0 is either be a normal python file or an executable. + # After installed as a package, sshuttle command points to an .exe in Windows and python shebang script elsewhere. argvbase = (([sys.executable, sys.argv[0]] if argv0.endswith('.py') else [argv0]) + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + @@ -234,43 +234,43 @@ def __init__(self, method_name, sudo_pythonpath): # Because underlying ShellExecute() Windows api does not allow child process to inherit stdio. # TODO(nom3ad): Try to implement another way to achieve this. raise Fatal("Privilege elevation for Windows is not yet implemented. Please run from an administrator shell") + + # Linux typically uses sudo; OpenBSD uses doas. However, some + # Linux distributions are starting to use doas. + sudo_cmd = ['sudo', '-p', '[local sudo] Password: '] + doas_cmd = ['doas'] + + # For clarity, try to replace executable name with the + # full path. + doas_path = which("doas") + if doas_path: + doas_cmd[0] = doas_path + sudo_path = which("sudo") + if sudo_path: + sudo_cmd[0] = sudo_path + + # sudo_pythonpath indicates if we should set the + # PYTHONPATH environment variable when elevating + # privileges. This can be adjusted with the + # --no-sudo-pythonpath option. + if sudo_pythonpath: + pp_prefix = ['/usr/bin/env', + 'PYTHONPATH=%s' % + os.path.dirname(os.path.dirname(__file__))] + sudo_cmd = sudo_cmd + pp_prefix + doas_cmd = doas_cmd + pp_prefix + + # Final order should be: sudo/doas command, env + # pythonpath, and then argvbase (sshuttle command). + sudo_cmd = sudo_cmd + argvbase + doas_cmd = doas_cmd + argvbase + + # If we can find doas and not sudo or if we are on + # OpenBSD, try using doas first. + if (doas_path and not sudo_path) or platform.platform().startswith('OpenBSD'): + argv_tries = [doas_cmd, sudo_cmd, argvbase] else: - # Linux typically uses sudo; OpenBSD uses doas. However, some - # Linux distributions are starting to use doas. - sudo_cmd = ['sudo', '-p', '[local sudo] Password: '] - doas_cmd = ['doas'] - - # For clarity, try to replace executable name with the - # full path. - doas_path = which("doas") - if doas_path: - doas_cmd[0] = doas_path - sudo_path = which("sudo") - if sudo_path: - sudo_cmd[0] = sudo_path - - # sudo_pythonpath indicates if we should set the - # PYTHONPATH environment variable when elevating - # privileges. This can be adjusted with the - # --no-sudo-pythonpath option. - if sudo_pythonpath: - pp_prefix = ['/usr/bin/env', - 'PYTHONPATH=%s' % - os.path.dirname(os.path.dirname(__file__))] - sudo_cmd = sudo_cmd + pp_prefix - doas_cmd = doas_cmd + pp_prefix - - # Final order should be: sudo/doas command, env - # pythonpath, and then argvbase (sshuttle command). - sudo_cmd = sudo_cmd + argvbase - doas_cmd = doas_cmd + argvbase - - # If we can find doas and not sudo or if we are on - # OpenBSD, try using doas first. - if (doas_path and not sudo_path) or platform.platform().startswith('OpenBSD'): - argv_tries = [doas_cmd, sudo_cmd, argvbase] - else: - argv_tries = [sudo_cmd, doas_cmd, argvbase] + argv_tries = [sudo_cmd, doas_cmd, argvbase] # Try all commands in argv_tries in order. If a command # produces an error, try the next one. If command is @@ -874,7 +874,7 @@ def main(listenip_v6, listenip_v4, # listenip_v4 contains user specified value or it is set to "auto". if listenip_v4 == "auto": - listenip_v4 = ('127.0.0.1' if avail.loopback_port else '0.0.0.0', 0) + listenip_v4 = ('127.0.0.1' if avail.loopback_proxy_port else '0.0.0.0', 0) debug1("Using default IPv4 listen address " + listenip_v4[0]) # listenip_v6 is... @@ -885,7 +885,7 @@ def main(listenip_v6, listenip_v4, debug1("IPv6 disabled by --disable-ipv6") if listenip_v6 == "auto": if avail.ipv6: - listenip_v6 = ('::1' if avail.loopback_port else '::', 0) + listenip_v6 = ('::1' if avail.loopback_proxy_port else '::', 0) debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0]) else: debug1("IPv6 disabled since it isn't supported by method " diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 519216dcc..969228ce5 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -242,7 +242,7 @@ def is_admin_user(): except Exception: return False - # TODO(nom3ad): for sys.platform == 'linux', support capabilities check for non-root users. (CAP_NET_ADMIN might be enough?) + # TODO(nom3ad): for sys.platform == 'linux', check capabilities for non-root users. (CAP_NET_ADMIN might be enough?) return os.getuid() == 0 diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index a654e70b4..49da095ea 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -46,7 +46,7 @@ def set_firewall(self, firewall): @staticmethod def get_supported_features(): result = Features() - result.loopback_port = True + result.loopback_proxy_port = True result.ipv4 = True result.ipv6 = False result.udp = False diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 389c3b72d..a962febf9 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -15,7 +15,7 @@ from sshuttle.methods import BaseMethod -from sshuttle.helpers import debug3, log, debug1, debug2, get_verbose_level, Fatal +from sshuttle.helpers import debug3, debug1, debug2, get_verbose_level, Fatal try: # https://reqrypt.org/windivert-doc.html#divert_iphdr @@ -47,6 +47,10 @@ class IPFamily(IntEnum): IPv4 = socket.AF_INET IPv6 = socket.AF_INET6 + @staticmethod + def from_ip_version(version): + return IPFamily.IPv6 if version == 4 else IPFamily.IPv4 + @property def filter(self): return "ip" if self == socket.AF_INET else "ipv6" @@ -280,7 +284,7 @@ class Method(BaseMethod): def __init__(self, name): super().__init__(name) - def _get_bind_addresses_for_port(self, port, family): + def _get_bind_address_for_port(self, port, family): proto = "TCPv6" if family.version == 6 else "TCP" for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): try: @@ -293,7 +297,7 @@ def _get_bind_addresses_for_port(self, port, family): raise Fatal("Could not find listening address for {}/{}".format(port, proto)) def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark): - log(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + debug2(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") if nslist or user or udp: raise NotImplementedError() @@ -304,18 +308,21 @@ def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user # using loopback only proxy binding won't work with windivert. # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 https://github.com/basil00/Divert/issues/82) # As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address) - local_addr = self._get_bind_addresses_for_port(proxy_port, family) - for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): - if addr.version != family.version or addr.is_loopback or addr.is_link_local: - continue - if local_addr.is_unspecified or local_addr == addr: - proxy_ip = addr.exploded - debug2("Found non loopback address to connect to proxy: " + proxy_ip) - break + proxy_bind_addr = self._get_bind_address_for_port(proxy_port, family) + if proxy_bind_addr.is_loopback: + raise Fatal("Windivert method requires proxy to be reachable by a non loopback address.") + if not proxy_bind_addr.is_unspecified: + proxy_ip = proxy_bind_addr.exploded else: - raise Fatal("Windivert method requires proxy to be reachable by a non loopback address." - f"No addersss found for {family.name}") - + local_addresses = [ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), 0, family=family)] + for addr in local_addresses: + if not addr.is_loopback and not addr.is_link_local: + proxy_ip = addr.exploded + break + else: + raise Fatal("Windivert method requires proxy to be reachable by a non loopback address." + f"No address found for {family.name} in {local_addresses}") + debug2("Found non loopback address to connect to proxy: " + proxy_ip) subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: if exclude: @@ -357,9 +364,11 @@ def restore_firewall(self, port, family, udp, user, group): def get_supported_features(self): result = super(Method, self).get_supported_features() - result.loopback_port = False + result.loopback_proxy_port = False result.user = False result.dns = False + # ipv6 only able to support with Windivert 2.x due to bugs in filter parsing + # TODO(nom3ad): Enable ipv6 once https://github.com/ffalcinelli/pydivert/pull/57 merged result.ipv6 = False return result @@ -463,19 +472,20 @@ def _ingress_divert(self, ready_cb): for pkt in w: verbose >= 3 and debug3("[INGRESS] " + repr_pkt(pkt)) if pkt.tcp.syn and pkt.tcp.ack: - # SYN+ACK received (connection established) + # SYN+ACK received (connection established from proxy conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) elif pkt.tcp.rst: - # RST received - Abrupt connection teardown initiated by otherside. We don't expect anymore packets + # RST received - Abrupt connection teardown initiated by proxy. Don't expect anymore packets conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) # https://wiki.wireshark.org/TCP-4-times-close.md elif pkt.tcp.fin and pkt.tcp.ack: - # FIN+ACK received (Passive close by otherside. We don't expect any more packets. Otherside expects an ACK) + # FIN+ACK received (Passive close by proxy. Don't expect any more packets. proxy expects an ACK) conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port) elif pkt.tcp.fin: - # FIN received (Otherside initiated graceful close. We expects a final ACK for a FIN packet) + # FIN received (proxy initiated graceful close. Expect a final ACK for a FIN packet) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT) else: + # data fragments and ACKs conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port) if not conn: verbose >= 2 and debug2("Unexpected packet: " + repr_pkt(pkt)) diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 08ea5df04..268442aca 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -262,7 +262,7 @@ def stream_sock_to_stdin(): threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin', daemon=True).start() return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0) - # https://stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows + # See: stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows close_fds = False if sys.platform == 'win32' else True debug2("executing: %r" % argv) From 89a94ff150a13f70f12fe6ff6b944d24e61301fe Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:22:50 +0530 Subject: [PATCH 199/275] support port ranges and exclude subnets --- sshuttle/methods/windivert.py | 89 +++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index a962febf9..d9d152418 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -7,7 +7,7 @@ import subprocess import re from multiprocessing import shared_memory -import struct +from struct import Struct from functools import wraps from enum import IntEnum import time @@ -61,7 +61,7 @@ def version(self): @property def loopback_addr(self): - return "127.0.0.1" if self == socket.AF_INET else "::1" + return ip_address("127.0.0.1" if self == socket.AF_INET else "::1") class ConnState(IntEnum): @@ -123,9 +123,9 @@ def __new__(cls, *args, **kwargs): raise RuntimeError("ConnTrack can not be instantiated multiple times") def __init__(self, name, max_connections=0) -> None: - self.struct_full_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B"))) - self.struct_src_tuple = struct.Struct(">" + "".join(("B", "B", "16s", "H"))) - self.struct_state_tuple = struct.Struct(">" + "".join(("L", "B"))) + self.struct_full_tuple = Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B"))) + self.struct_src_tuple = Struct(">" + "".join(("B", "B", "16s", "H"))) + self.struct_state_tuple = Struct(">" + "".join(("L", "B"))) try: self.max_connections = max_connections @@ -142,8 +142,8 @@ def __init__(self, name, max_connections=0) -> None: self.max_connections = len(self.shm_list) debug2( - f"ConnTrack: is_owner={self.is_owner} entry_size={self.struct_full_tuple.size} shm_name={self.shm_list.shm.name} " - f"shm_size={self.shm_list.shm.size}B" + f"ConnTrack: is_owner={self.is_owner} cap={len(self.shm_list)} item_sz={self.struct_full_tuple.size}B" + f"shm_name={self.shm_list.shm.name} shm_sz={self.shm_list.shm.size}B" ) @synchronized_method("rlock") @@ -279,7 +279,6 @@ def __repr__(self): class Method(BaseMethod): network_config = {} - proxy_port = None def __init__(self, name): super().__init__(name) @@ -297,10 +296,10 @@ def _get_bind_address_for_port(self, port, family): raise Fatal("Could not find listening address for {}/{}".format(port, proto)) def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark): - debug2(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") + debug2(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {group=} {tmark=}") - if nslist or user or udp: - raise NotImplementedError() + if nslist or user or udp or group: + raise NotImplementedError("user, group, nslist, udp are not supported") family = IPFamily(family) @@ -312,24 +311,26 @@ def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user if proxy_bind_addr.is_loopback: raise Fatal("Windivert method requires proxy to be reachable by a non loopback address.") if not proxy_bind_addr.is_unspecified: - proxy_ip = proxy_bind_addr.exploded + proxy_ip = proxy_bind_addr else: local_addresses = [ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), 0, family=family)] for addr in local_addresses: if not addr.is_loopback and not addr.is_link_local: - proxy_ip = addr.exploded + proxy_ip = addr break else: raise Fatal("Windivert method requires proxy to be reachable by a non loopback address." f"No address found for {family.name} in {local_addresses}") - debug2("Found non loopback address to connect to proxy: " + proxy_ip) + debug2(f"Found non loopback address to connect to proxy: {proxy_ip}") subnet_addresses = [] for (_, mask, exclude, network_addr, fport, lport) in subnets: - if exclude: - continue - assert fport == 0, "custom port range not supported" - assert lport == 0, "custom port range not supported" - subnet_addresses.append("%s/%s" % (network_addr, mask)) + if fport and lport: + if lport > fport: + raise Fatal("lport must be less than or equal to fport") + ports = (fport, lport) + else: + ports = None + subnet_addresses.append((ip_network(f"{network_addr}/{mask}"), ports, exclude)) self.network_config[family] = { "subnets": subnet_addresses, @@ -391,30 +392,48 @@ def _egress_divert(self, ready_cb): """divert outgoing packets to proxy""" proto = IPProtocol.TCP filter = f"outbound and {proto.filter}" - - # with pydivert.WinDivert(f"outbound and tcp and ip.DstAddr == {subnet}") as w: - family_filters = [] + af_filters = [] for af, c in self.network_config.items(): - subnet_filters = [] - for cidr in c["subnets"]: - ip_net = ip_network(cidr) + subnet_include_filters = [] + subnet_exclude_filters = [] + for ip_net, ports, exclude in c["subnets"]: first_ip = ip_net.network_address.exploded last_ip = ip_net.broadcast_address.exploded - subnet_filters.append(f"({af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip})") - if not subnet_filters: - continue + if first_ip == last_ip: + _subney_filter = f"{af.filter}.DstAddr=={first_ip}" + else: + _subney_filter = f"{af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip}" + if ports: + if ports[0] == ports[1]: + _subney_filter += f" and {proto.filter}.DstPort=={ports[0]}" + else: + _subney_filter += f" and tcp.DstPort>={ports[0]} and tcp.DstPort<={ports[1]}" + (subnet_exclude_filters if exclude else subnet_include_filters).append(f'({_subney_filter})') + _af_filter = f"{af.filter}" + if subnet_include_filters: + _af_filter += f" and ({' or '.join(subnet_include_filters)})" + if subnet_exclude_filters: + # TODO(noma3ad) use not() operator with Windivert2 after upgrade + _af_filter += f" and (({' or '.join(subnet_exclude_filters)})? false : true)" proxy_ip, proxy_port = c["proxy_addr"] - proxy_guard_filter = f'({af.filter}.DstAddr!={proxy_ip} or tcp.DstPort!={proxy_port})' - family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) and {proxy_guard_filter}") - if not family_filters: + # Avoids proxy outbound traffic getting directed to itself + proxy_guard_filter = f'(({af.filter}.DstAddr=={proxy_ip.exploded} and tcp.DstPort=={proxy_port})? false : true)' + _af_filter += f" and {proxy_guard_filter}" + af_filters.append(_af_filter) + if not af_filters: raise Fatal("At least one ipv4 or ipv6 subnet is expected") - filter = f"{filter} and ({' or '.join(family_filters)})" + filter = f"{filter} and ({' or '.join(af_filters)})" debug1(f"[EGRESS] {filter=}") with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: + proxy_ipv4, proxy_ipv6 = None, None + if IPFamily.IPv4 in self.network_config: + proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"] + proxy_ipv4 = proxy_ipv4[0].exploded, proxy_ipv4[1] + if IPFamily.IPv6 in self.network_config: + proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"] + proxy_ipv6 = proxy_ipv6[0].exploded, proxy_ipv6[1] ready_cb() - proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"] if IPFamily.IPv4 in self.network_config else None - proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"] if IPFamily.IPv6 in self.network_config else None verbose = get_verbose_level() for pkt in w: verbose >= 3 and debug3("[EGRESS] " + repr_pkt(pkt)) @@ -461,7 +480,7 @@ def _ingress_divert(self, ready_cb): continue proxy_ip, proxy_port = c["proxy_addr"] # "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this - proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip} and tcp.SrcPort=={proxy_port}") + proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip.exploded} and tcp.SrcPort=={proxy_port}") if not proxy_addr_filters: raise Fatal("At least one ipv4 or ipv6 address is expected") filter = f"{direction} and {proto.filter} and ({' or '.join(proxy_addr_filters)})" From 81a598a4cc68e4d26f37fb13a81667339c1e33ec Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:38:26 +0530 Subject: [PATCH 200/275] suppport --auto-hosts in Windows --- sshuttle/firewall.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 2ec9e2538..bbeaaa4b9 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -15,7 +15,10 @@ from sshuttle.helpers import is_admin_user, log, debug1, debug2, debug3, Fatal from sshuttle.methods import get_auto_method, get_method -HOSTSFILE = '/etc/hosts' +if sys.platform == 'win32': + HOSTSFILE = r"C:\Windows\System32\drivers\etc\hosts" +else: + HOSTSFILE = '/etc/hosts' sshuttle_pid = None @@ -48,12 +51,13 @@ def rewrite_etc_hosts(hostmap, port): f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND)) f.close() - if st is not None: - os.chown(tmpname, st.st_uid, st.st_gid) - os.chmod(tmpname, st.st_mode) - else: - os.chown(tmpname, 0, 0) - os.chmod(tmpname, 0o644) + if sys.platform != 'win32': + if st is not None: + os.chown(tmpname, st.st_uid, st.st_gid) + os.chmod(tmpname, st.st_mode) + else: + os.chown(tmpname, 0, 0) + os.chmod(tmpname, 0o644) try: os.rename(tmpname, HOSTSFILE) except OSError: From 7a92183f5950262102b68aadf23c0cd35d15afc0 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Tue, 9 Jan 2024 23:04:04 +0530 Subject: [PATCH 201/275] windows: better connection tracker --- sshuttle/firewall.py | 2 +- sshuttle/methods/__init__.py | 2 +- sshuttle/methods/windivert.py | 29 +++++++++++++++++++++-------- tests/client/test_firewall.py | 2 +- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index bbeaaa4b9..0b34f6882 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -348,7 +348,7 @@ def _read_next_string_line(): try: # For some methods (eg: windivert) firewall setup will be differed / will run asynchronously. # Such method implements wait_for_firewall_ready() to wait until firewall is up and running. - method.wait_for_firewall_ready() + method.wait_for_firewall_ready(sshuttle_pid) except NotImplementedError: pass diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 49da095ea..0f56e59ae 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -98,7 +98,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def restore_firewall(self, port, family, udp, user, group): raise NotImplementedError() - def wait_for_firewall_ready(self): + def wait_for_firewall_ready(self, sshuttle_pid): raise NotImplementedError() @staticmethod diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index d9d152418..2bf4674e2 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -15,7 +15,7 @@ from sshuttle.methods import BaseMethod -from sshuttle.helpers import debug3, debug1, debug2, get_verbose_level, Fatal +from sshuttle.helpers import log, debug3, debug1, debug2, get_verbose_level, Fatal try: # https://reqrypt.org/windivert-doc.html#divert_iphdr @@ -228,8 +228,17 @@ def get(self, proto, src_addr, src_port): if entry and entry.startswith(packed): return self._unpack(entry) + def dump(self): + for entry in self.shm_list: + if not entry: + continue + conn = self._unpack(entry) + proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state = conn + log(f"{proto.name}/{ip_version} {src_addr}:{src_port} -> {dst_addr}:{dst_port} {state.name}@{state_epoch}") + @synchronized_method("rlock") def gc(self, connection_timeout_sec=15): + # self.dump() now = int(time.time()) n = 0 for i in tuple(self.used_slots): @@ -261,9 +270,9 @@ def _unpack(self, packed): ) = self.struct_full_tuple.unpack(packed) dst_addr = ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4]).exploded src_addr = ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4]).exploded - return ConnectionTuple( - IPProtocol(proto), ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, ConnState(state) - ) + proto = IPProtocol(proto) + state = ConnState(state) + return ConnectionTuple(proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state) def __iter__(self): def conn_iter(): @@ -338,12 +347,14 @@ def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user "proxy_addr": (proxy_ip, proxy_port) } - def wait_for_firewall_ready(self): + def wait_for_firewall_ready(self, sshuttle_pid): debug2(f"network_config={self.network_config}") - self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) - methods = (self._egress_divert, self._ingress_divert, self._connection_gc) + self.conntrack = ConnTrack(f"sshuttle-windivert-{sshuttle_pid}", WINDIVERT_MAX_CONNECTIONS) + if not self.conntrack.is_owner: + raise Fatal("ConnTrack should be owner in wait_for_firewall_ready()") + thread_target_funcs = (self._egress_divert, self._ingress_divert, self._connection_gc) ready_events = [] - for fn in methods: + for fn in thread_target_funcs: ev = threading.Event() ready_events.append(ev) @@ -376,6 +387,8 @@ def get_supported_features(self): def get_tcp_dstip(self, sock): if not hasattr(self, "conntrack"): self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}") + if self.conntrack.is_owner: + raise Fatal("ConnTrack should not be owner in get_tcp_dstip()") src_addr, src_port = sock.getpeername() c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port) diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index f9d4fdb79..a953527bc 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -157,7 +157,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): None, None, '0x01'), - call().wait_for_firewall_ready(), + call().wait_for_firewall_ready(os.getpid()), call().restore_firewall(1024, AF_INET6, True, None, None), call().restore_firewall(1025, AF_INET, True, None, None), ] From 6b8e40236708b1ba503864786c75062b5d0b8ce5 Mon Sep 17 00:00:00 2001 From: nom3ad <19239479+nom3ad@users.noreply.github.com> Date: Wed, 10 Jan 2024 16:24:33 +0530 Subject: [PATCH 202/275] make sure that existing python2 compatibility is not broken by this feature --- scripts/Containerfile | 35 +++++++++-- scripts/README.md | 8 ++- scripts/{setup.service => container.setup.sh} | 23 +++++--- scripts/exec-sshuttle | 59 +++++++++++++++---- scripts/exec-tool | 28 +++++---- scripts/run-benchmark | 20 ++++--- scripts/test-bed | 2 +- sshuttle/methods/windivert.py | 12 ++-- sshuttle/server.py | 3 +- sshuttle/ssh.py | 5 +- sshuttle/ssnet.py | 6 +- tests/client/test_firewall.py | 2 +- 12 files changed, 148 insertions(+), 55 deletions(-) rename scripts/{setup.service => container.setup.sh} (74%) diff --git a/scripts/Containerfile b/scripts/Containerfile index 29e63052f..9bf96e857 100644 --- a/scripts/Containerfile +++ b/scripts/Containerfile @@ -1,15 +1,40 @@ -FROM docker.io/linuxserver/openssh-server:latest # https://hub.docker.com/r/linuxserver/openssh-server/ +ARG BASE_IMAGE=docker.io/linuxserver/openssh-server:version-9.3_p2-r1 -RUN apk add --no-cache bash python3 nginx iperf3 +FROM ${BASE_IMAGE} as pyenv -# suppress linuxserver.io logo printing -RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run +# https://github.com/pyenv/pyenv/wiki#suggested-build-environment +RUN apk add --no-cache build-base git libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev +ENV PYENV_ROOT=/pyenv +RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash +RUN /pyenv/bin/pyenv install 2.7 +RUN /pyenv/bin/pyenv install 3.6 +RUN /pyenv/bin/pyenv install 3.8 +RUN /pyenv/bin/pyenv install 3.10 +RUN bash -xc 'rm -rf /pyenv/{.git,plugins} /pyenv/versions/*/lib/*/{test,config,config-*linux-gnu}' && \ + find /pyenv -type d -name __pycache__ -exec rm -rf {} + && \ + find /pyenv -type f -name '*.py[co]' -delete +FROM ${BASE_IMAGE} + +RUN apk add --no-cache bash nginx iperf3 + +# pyenv setup +ENV PYENV_ROOT=/pyenv +ENV PATH=/pyenv/shims:/pyenv/bin:$PATH +COPY --from=pyenv /pyenv /pyenv + +# OpenSSH Server variables ENV PUID=1000 ENV PGID=1000 ENV PASSWORD_ACCESS=true ENV USER_NAME=test ENV USER_PASSWORD=test ENV LOG_STDOUT=true -COPY ./setup.service /etc/services.d/setup.service/run \ No newline at end of file + +# suppress linuxserver.io logo printing, chnage sshd config +RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run + +# https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers +# To customize the container and start other components +COPY container.setup.sh /custom-cont-init.d/setup.sh \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index f335d431a..eb4ad0485 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -3,11 +3,17 @@ ```bash test-bed up -d # start containers -exec-sshuttle [--copy-id] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...] +exec-sshuttle [--copy-id] [--server-py=2.7|3.6|3.8] [--client-py=2.7|3.6|3.8] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...] + # --copy-id -> optionally do ssh-copy-id to make it passwordless + # --sshuttle-bin -> use another sshuttle binary instead of one from dev setup + # --server-py -> Python version to use in server. (manged by pyenv) + # --client-py -> Python version to use in client (manged by pyenv) exec-sshuttle node-1 # start sshuttle to connect to node-1 exec-tool curl node-1 # curl to nginx instance running on node1 via IP that is only reachable via sshuttle exec-tool iperf3 node-1 # measure throughput to node-1 +run-benchmark node-1 --client-py=3.10 + ``` diff --git a/scripts/setup.service b/scripts/container.setup.sh similarity index 74% rename from scripts/setup.service rename to scripts/container.setup.sh index fb4935394..255e22f9a 100755 --- a/scripts/setup.service +++ b/scripts/container.setup.sh @@ -3,8 +3,6 @@ set -e -echo -e ">>> Setting up $(hostname) | id: $(id) | $(python --version) \nip: $(ip a)\n route: $(ip r)" - function with_set_x() { set -x "$@" @@ -15,20 +13,31 @@ function with_set_x() { } 2>/dev/null } + +function log() { + echo "$*" >&2 +} + +log ">>> Setting up $(hostname) | id: $(id)\nIP:\n$(ip a)\nRoutes:\n$(ip r)\npyenv:\n$(pyenv versions)" + +echo " +AcceptEnv PYENV_VERSION +" >> /etc/ssh/sshd_config + iface="$(ip route | awk '/default/ { print $5 }')" default_gw="$(ip route | awk '/default/ { print $3 }')" for addr in ${ADD_IP_ADDRESSES//,/ }; do - echo ">>> Adding $addr to interface $iface" + log ">>> Adding $addr to interface $iface" net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}') with_set_x ip addr add "$addr" dev "$iface" with_set_x ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes done -echo ">>> Starting iperf3 server" +log ">>> Starting iperf3 server" iperf3 --server --port 5001 & mkdir -p /www -echo -e "
Hello from $(hostname)
+echo "
Hello from $(hostname)
 ip address
 $(ip address)
@@ -52,5 +61,5 @@ http {
     }
 }" >/etc/nginx/nginx.conf
 
-echo ">>> Starting nginx"
-exec nginx
+log ">>> Starting nginx"
+nginx &
diff --git a/scripts/exec-sshuttle b/scripts/exec-sshuttle
index 660bed26f..343fe459f 100755
--- a/scripts/exec-sshuttle
+++ b/scripts/exec-sshuttle
@@ -11,7 +11,12 @@ function with_set_x() {
     } 2>/dev/null
 }
 
+function log() {
+    echo "$*" >&2
+}
+
 ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
+ssh_copy_id=false
 args=()
 while [[ $# -gt 0 ]]; do
     arg=$1
@@ -20,10 +25,23 @@ while [[ $# -gt 0 ]]; do
     -v)
         ssh_cmd+='-v'
         ;;
+    -r)
+        args+=("$arg" "$1")
+        shift
+        continue
+        ;;
     --copy-id)
         ssh_copy_id=true
         continue
         ;;
+    --server-py=*)
+        server_pyenv_ver="${arg#*=}"
+        continue
+        ;;
+    --client-py=*)
+        client_pyenv_ver="${arg#*=}"
+        continue
+        ;;
     -6)
         ipv6_only=true
         continue
@@ -47,6 +65,9 @@ port="2222"
 user="test:test"
 
 if [[ $node == node-* ]]; then
+    pycmd="/pyenv/shims/python"
+    ssh_cmd+=" -o SetEnv=PYENV_VERSION=${server_pyenv_ver:-'3'}"
+    args=("--python=$pycmd" "${args[@]}")
     host=$("$(dirname "$0")/test-bed" get-ip "$node")
     index=${node#node-}
     if [[ $ipv6_only == true ]]; then
@@ -58,27 +79,45 @@ else
     host=$node
 fi
 
-if [[ "${#args[@]}" -ne 0  && "${args[$((${#args[@]} - 1))]}" != *.* && "${args[$((${#args[@]} - 1))]}" != *:* ]]; then
-    echo "No subnet specified. Using -N" >&2
-    args+=('-N')
-fi
-
 if ! command -v sshpass >/dev/null; then
-    echo "sshpass is not found. You might have to manually enter ssh password: 'test'" >&2
+    log "sshpass is not found. You might have to manually enter ssh password: 'test'"
     user=${user%:*}
 fi
 
 if [[ $ssh_copy_id == true ]]; then
-    echo "Trying to make it passwordless" >&2
+    log "Trying to make it passwordless"
     with_set_x ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host"
 fi
 
 if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then
     cd "$(dirname "$0")/.."
     export PYTHONPATH="."
-    sshuttle_bin="./run"
+    if [[ -n $client_pyenv_ver ]]; then
+        log "Using pyenv version: $client_pyenv_ver"
+        command -v pyenv &>/dev/null || log "You have to install pyenv to use --client-py" && exit 1
+        sshuttle_cmd=(/usr/bin/env PYENV_VERSION="$client_pyenv_ver" pyenv exec python -m sshuttle)
+    else
+        log "Using best python version availble"
+        if [ -x "$(command -v python3)" ] &&
+            python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
+            sshuttle_cmd=(python3 -m sshuttle)
+        else
+            sshuttle_cmd=(python -m sshuttle)
+        fi
+    fi
+else
+    [[ -n $client_pyenv_ver ]] && log "Can't specify --client-py when --sshuttle-bin is specified" && exit 1
+    sshuttle_cmd=("$sshuttle_bin")
+fi
+
+if [[ " ${args[*]} " != *" --ssh-cmd "* ]]; then
+    args=("--ssh-cmd" "$ssh_cmd" "${args[@]}")
+fi
+
+if [[ " ${args[*]} " != *" -r "* ]]; then
+    args=("-r" "$user@$host:$port" "${args[@]}")
 fi
 
 set -x
-$sshuttle_bin --version
-exec "${sshuttle_bin}" -r "$user@$host:$port" --ssh-cmd "$ssh_cmd" "${args[@]}"
+"${sshuttle_cmd[@]}" --version
+exec "${sshuttle_cmd[@]}" "${args[@]}"
diff --git a/scripts/exec-tool b/scripts/exec-tool
index fc4eedc59..17ab51599 100755
--- a/scripts/exec-tool
+++ b/scripts/exec-tool
@@ -1,6 +1,22 @@
 #!/usr/bin/env bash
 set -e
 
+
+function with_set_x() {
+    set -x
+    "$@"
+    {
+        ec=$?
+        set +x
+        return $ec
+    } 2>/dev/null
+}
+
+function log() {
+    echo "$*" >&2
+}
+
+
 args=()
 while [[ $# -gt 0 ]]; do
     arg=$1
@@ -40,16 +56,6 @@ fi
 
 connect_timeout_sec=3
 
-function with_set_x() {
-    set -x
-    "$@"
-    {
-        ec=$?
-        set +x
-        return $ec
-    } 2>/dev/null
-}
-
 case "$tool" in
 ping)
     with_set_x exec ping -W $connect_timeout_sec "${args[@]}" "$host"
@@ -74,7 +80,7 @@ ab)
     with_set_x exec ab -s $connect_timeout_sec "${args[@]}" "http://$host:$port/"
     ;;
 *)
-    echo "Unknown tool: $tool" >&2
+    log "Unknown tool: $tool"
     exit 2
     ;;
 esac
diff --git a/scripts/run-benchmark b/scripts/run-benchmark
index 94600bfb3..4f51b8a54 100755
--- a/scripts/run-benchmark
+++ b/scripts/run-benchmark
@@ -12,13 +12,17 @@ function with_set_x() {
     } 2>/dev/null
 }
 
+function log() {
+    echo "$*" >&2
+}
+
 ./test-bed up -d
 
 benchmark() {
-    local sshuttle_bin="${1?:}"
-    local node="${2:-"node-1"}"
-    echo -e "\n======== Benchmarking sshuttle: $sshuttle_bin  ========"
-    with_set_x ./exec-sshuttle "$node" --sshuttle-bin="$sshuttle_bin" --listen 55771 &
+    log -e "\n======== Benchmarking sshuttle | Args: [$*]  ========"
+    local node=$1
+    shift
+    with_set_x ./exec-sshuttle "$node" --listen 55771 "$@" &
     sshuttle_pid=$!
     trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT
     while ! nc -z localhost 55771; do sleep 0.1; done
@@ -28,9 +32,9 @@ benchmark() {
     wait $sshuttle_pid || true
 }
 
-if [[ "$1" ]]; then
-    benchmark "$1"
+if [[ $# -gt 0 ]]; then
+    benchmark "${@}"
 else
-    benchmark "${SSHUTTLE_BIN:-sshuttle}" node-1
-    benchmark dev node-1
+    benchmark node-1 --sshuttle-bin="${SSHUTTLE_BIN:-sshuttle}"
+    benchmark node-1 --sshuttle-bin=dev
 fi
diff --git a/scripts/test-bed b/scripts/test-bed
index cb0504317..7877b9e0a 100755
--- a/scripts/test-bed
+++ b/scripts/test-bed
@@ -18,7 +18,7 @@ function with_set_x() {
 
 function build() {
     # podman build -t ghcr.io/sshuttle/sshuttle-testbed .
-    with_set_x docker build -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile .
+    with_set_x docker build --progress=plain -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile .
 }
 
 function compose() {
diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py
index 2bf4674e2..fa61709c0 100644
--- a/sshuttle/methods/windivert.py
+++ b/sshuttle/methods/windivert.py
@@ -413,15 +413,15 @@ def _egress_divert(self, ready_cb):
                 first_ip = ip_net.network_address.exploded
                 last_ip = ip_net.broadcast_address.exploded
                 if first_ip == last_ip:
-                    _subney_filter = f"{af.filter}.DstAddr=={first_ip}"
+                    _subnet_filter = f"{af.filter}.DstAddr=={first_ip}"
                 else:
-                    _subney_filter = f"{af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip}"
+                    _subnet_filter = f"{af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip}"
                 if ports:
                     if ports[0] == ports[1]:
-                        _subney_filter += f" and {proto.filter}.DstPort=={ports[0]}"
+                        _subnet_filter += f" and {proto.filter}.DstPort=={ports[0]}"
                     else:
-                        _subney_filter += f" and tcp.DstPort>={ports[0]} and tcp.DstPort<={ports[1]}"
-                (subnet_exclude_filters if exclude else subnet_include_filters).append(f'({_subney_filter})')
+                        _subnet_filter += f" and tcp.DstPort>={ports[0]} and tcp.DstPort<={ports[1]}"
+                (subnet_exclude_filters if exclude else subnet_include_filters).append(f"({_subnet_filter})")
             _af_filter = f"{af.filter}"
             if subnet_include_filters:
                 _af_filter += f" and ({' or '.join(subnet_include_filters)})"
@@ -430,7 +430,7 @@ def _egress_divert(self, ready_cb):
                 _af_filter += f" and (({' or '.join(subnet_exclude_filters)})? false : true)"
             proxy_ip, proxy_port = c["proxy_addr"]
             # Avoids proxy outbound traffic getting directed to itself
-            proxy_guard_filter = f'(({af.filter}.DstAddr=={proxy_ip.exploded} and tcp.DstPort=={proxy_port})? false : true)'
+            proxy_guard_filter = f"(({af.filter}.DstAddr=={proxy_ip.exploded} and tcp.DstPort=={proxy_port})? false : true)"
             _af_filter += f" and {proxy_guard_filter}"
             af_filters.append(_af_filter)
         if not af_filters:
diff --git a/sshuttle/server.py b/sshuttle/server.py
index 867c041d8..ddca1f356 100644
--- a/sshuttle/server.py
+++ b/sshuttle/server.py
@@ -5,6 +5,7 @@
 import time
 import sys
 import os
+import io
 
 
 import sshuttle.ssnet as ssnet
@@ -281,7 +282,7 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
         sys.stdout.flush()
 
         handlers = []
-        mux = Mux(sys.stdin.buffer, sys.stdout.buffer)
+        mux = Mux(io.FileIO(0, mode='r'), io.FileIO(1, mode='w'))
         handlers.append(mux)
 
         debug1('auto-nets:' + str(auto_nets))
diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index 268442aca..b8b947a99 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -240,7 +240,8 @@ def stream_stdout_to_sock():
                     fd = p.stdout.fileno()
                     for data in iter(lambda: os.read(fd, 16384), b''):
                         s1.sendall(data)
-                        # debug3(f"<<<<< p.stdout.read() {len(data)} {data[:min(32,len(data))]}...")
+                        # debug3("<<<<< p.stdout.read() %d %r...", len(data), data[:min(32, len(data))])
+
                 finally:
                     debug2("Thread 'stream_stdout_to_sock' exiting")
                     s1.close()
@@ -249,7 +250,7 @@ def stream_stdout_to_sock():
             def stream_sock_to_stdin():
                 try:
                     for data in iter(lambda: s1.recv(16384), b''):
-                        # debug3(f">>>>> p.stdout.write() {len(data)} {data[:min(32,len(data))]}...")
+                        # debug3("<<<<< p.stdout.write() %d %r...", len(data), data[:min(32, len(data))])
                         while data:
                             n = p.stdin.write(data)
                             data = data[n:]
diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py
index 6110b4ca1..9438ce885 100644
--- a/sshuttle/ssnet.py
+++ b/sshuttle/ssnet.py
@@ -77,7 +77,8 @@ def _fds(socks):
 def _nb_clean(func, *args):
     try:
         return func(*args)
-    except OSError:
+    except (OSError, socket.error):
+        # Note: In python2 socket.error != OSError (In python3, they are same)
         _, e = sys.exc_info()[:2]
         if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
             raise
@@ -433,7 +434,7 @@ def flush(self):
         set_non_blocking_io(self.wfile.fileno())
         if self.outbuf and self.outbuf[0]:
             wrote = _nb_clean(self.wfile.write, self.outbuf[0])
-            self.wfile.flush()
+            # self.wfile.flush()
             debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
             if wrote:
                 self.outbuf[0] = self.outbuf[0][wrote:]
@@ -446,6 +447,7 @@ def fill(self):
             # If LATENCY_BUFFER_SIZE is inappropriately large, we will
             # get a MemoryError here. Read no more than 1MiB.
             read = _nb_clean(self.rfile.read, min(1048576, LATENCY_BUFFER_SIZE))
+            debug2('mux read: %r' % len(read))
         except OSError:
             _, e = sys.exc_info()[:2]
             raise Fatal('other end: %r' % e)
diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py
index a953527bc..7f702b372 100644
--- a/tests/client/test_firewall.py
+++ b/tests/client/test_firewall.py
@@ -157,7 +157,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
             None,
             None,
             '0x01'),
-        call().wait_for_firewall_ready(os.getpid()),
+        call().wait_for_firewall_ready(12345),
         call().restore_firewall(1024, AF_INET6, True, None, None),
         call().restore_firewall(1025, AF_INET, True, None, None),
     ]

From c4255a23f007dc361865a166ff6fe8d638938bbe Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Sun, 14 Apr 2024 12:40:46 +0530
Subject: [PATCH 203/275] update exec-sshuttle script

---
 scripts/README.md     |  4 +-
 scripts/exec-sshuttle | 98 +++++++++++++++++++++++++++++--------------
 2 files changed, 70 insertions(+), 32 deletions(-)

diff --git a/scripts/README.md b/scripts/README.md
index eb4ad0485..878d4f956 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -4,7 +4,7 @@
 test-bed up -d # start containers
 
 exec-sshuttle  [--copy-id] [--server-py=2.7|3.6|3.8] [--client-py=2.7|3.6|3.8] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...]
-    # --copy-id  -> optionally do ssh-copy-id to make it passwordless
+    # --copy-id  -> optionally do ssh-copy-id to make it passwordless for future runs
     # --sshuttle-bin -> use another sshuttle binary instead of one from dev setup
     # --server-py  -> Python version to use in server. (manged by pyenv)
     # --client-py -> Python version to use in client (manged by pyenv)
@@ -17,3 +17,5 @@ exec-tool iperf3 node-1 # measure throughput to node-1
 run-benchmark node-1 --client-py=3.10
 
 ```
+
+
diff --git a/scripts/exec-sshuttle b/scripts/exec-sshuttle
index 343fe459f..bd93495e1 100755
--- a/scripts/exec-sshuttle
+++ b/scripts/exec-sshuttle
@@ -1,6 +1,8 @@
 #!/usr/bin/env bash
 set -e
 
+ export MSYS_NO_PATHCONV=1
+ 
 function with_set_x() {
     set -x
     "$@"
@@ -18,75 +20,109 @@ function log() {
 ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
 ssh_copy_id=false
 args=()
+subnet_args=()
 while [[ $# -gt 0 ]]; do
     arg=$1
     shift
     case "$arg" in
-    -v)
-        ssh_cmd+='-v'
+    -v|-vv*)
+        ssh_cmd+=" -v"
+        args+=("$arg")
         ;;
     -r)
-        args+=("$arg" "$1")
+        args+=("-r" "$1")
         shift
-        continue
         ;;
     --copy-id)
         ssh_copy_id=true
-        continue
         ;;
     --server-py=*)
         server_pyenv_ver="${arg#*=}"
-        continue
         ;;
     --client-py=*)
         client_pyenv_ver="${arg#*=}"
-        continue
         ;;
     -6)
         ipv6_only=true
-        continue
         ;;
     --sshuttle-bin=*)
         sshuttle_bin="${arg#*=}"
-        continue
         ;;
-    -*) ;;
+    -N|*/*)
+        subnet_args+=("$arg")
+    ;;
+    -*) 
+        args+=("$arg")
+    ;;
     *)
-        if [[ -z "$node" ]]; then
-            node=$arg
-            continue
+        if  [[ -z "$target" ]]; then
+            target=$arg
+        else
+            args+=("$arg")
         fi
         ;;
     esac
-    args+=("$arg")
 done
+if [[ ${#subnet_args[@]} -eq 0 ]]; then
+    subnet_args=("-N")
+fi
 
-port="2222"
-user="test:test"
-
-if [[ $node == node-* ]]; then
-    pycmd="/pyenv/shims/python"
-    ssh_cmd+=" -o SetEnv=PYENV_VERSION=${server_pyenv_ver:-'3'}"
-    args=("--python=$pycmd" "${args[@]}")
-    host=$("$(dirname "$0")/test-bed" get-ip "$node")
-    index=${node#node-}
+if [[ $target == node-* ]]; then
+    log "Target is a a test-bed node"
+    port="2222"
+    user_part="test:test"
+    host=$("$(dirname "$0")/test-bed" get-ip "$target")
+    index=${target#node-}
     if [[ $ipv6_only == true ]]; then
         args+=("2001:0DB8::/112")
     else
         args+=("10.55.$index.0/24")
     fi
-else
-    host=$node
+    target="$user_part@$host:$port"
+    if ! command -v sshpass >/dev/null; then
+        log "sshpass is not found. You might have to manually enter ssh password: 'test'"
+    fi
+    if [[ -z $server_pyenv_ver ]]; then
+        log "server-py argumwnt is not specified. Setting it to 3.8"
+        server_pyenv_ver="3.8"
+    fi
 fi
 
-if ! command -v sshpass >/dev/null; then
-    log "sshpass is not found. You might have to manually enter ssh password: 'test'"
-    user=${user%:*}
+if [[ -n $server_pyenv_ver ]]; then
+    log "Would pass PYENV_VERRSION=$server_pyenv_ver to server. pyenv is required on server to make it work"
+    pycmd="/pyenv/shims/python"
+    ssh_cmd+=" -o SetEnv=PYENV_VERSION=${server_pyenv_ver:-'3'}"
+    args=("--python=$pycmd" "${args[@]}")
 fi
 
 if [[ $ssh_copy_id == true ]]; then
     log "Trying to make it passwordless"
-    with_set_x ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host"
+    if [[ $target == *@* ]]; then
+        user_part="${target%%@*}"
+        host_part="${target#*@}"
+    else
+        user_part="$(whoami)"
+        host_part="$target"
+    fi
+    if [[ $host_part == *:* ]]; then
+        host="${host_part%:*}"
+        port="${host_part#*:}"
+    else
+        host="$host_part"
+        port="22"
+    fi
+    if [[ $user_part == *:* ]]; then
+        user="${user_part%:*}"
+        password="${user_part#*:}"
+    else
+        user="$user_part"
+        password=""
+    fi
+    cmd=(ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host")
+    if [[ -n $password  ]] && command -v sshpass >/dev/null; then
+        cmd=(sshpass -p "$password" "${cmd[@]}")
+    fi
+    with_set_x "${cmd[@]}"
 fi
 
 if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then
@@ -115,9 +151,9 @@ if [[ " ${args[*]} " != *" --ssh-cmd "* ]]; then
 fi
 
 if [[ " ${args[*]} " != *" -r "* ]]; then
-    args=("-r" "$user@$host:$port" "${args[@]}")
+    args=("-r" "$target" "${args[@]}")
 fi
 
 set -x
 "${sshuttle_cmd[@]}" --version
-exec "${sshuttle_cmd[@]}" "${args[@]}"
+exec "${sshuttle_cmd[@]}" "${args[@]}" "${subnet_args[@]}"

From ace8642950dd29575e5992c99cd2e678534ae42a Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Sun, 14 Apr 2024 12:01:17 +0530
Subject: [PATCH 204/275] add SocketRWShim helper

---
 sshuttle/helpers.py | 47 ++++++++++++++++++++++++++++++++++++++++++++-
 sshuttle/ssh.py     | 34 +++-----------------------------
 2 files changed, 49 insertions(+), 32 deletions(-)

diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py
index 969228ce5..c1f4baf58 100644
--- a/sshuttle/helpers.py
+++ b/sshuttle/helpers.py
@@ -2,7 +2,8 @@
 import socket
 import errno
 import os
-
+import threading
+import traceback
 
 if sys.platform != "win32":
     import fcntl
@@ -275,3 +276,47 @@ def close(self):
                 f.close()
             except Exception:
                 pass
+
+
+class SocketRWShim:
+    __slots__ = ('_r', '_w', '_on_end', '_s1', '_s2', '_t1', '_t2')
+
+    def __init__(self, r, w, on_end=None) -> None:
+        self._r = r
+        self._w = w
+        self._on_end = on_end
+
+        self._s1, self._s2 = socket.socketpair()
+        debug3("[SocketShim] r=%r w=%r | s1=%r s2=%r" % (self._r, self._w, self._s1, self._s2))
+
+        def stream_reader_to_sock():
+            try:
+                for data in iter(lambda:  self._r.read(16384), b''):
+                    self._s1.sendall(data)
+                    # debug3("[SocketRWShim] <<<<< r.read() %d %r..." % (len(data), data[:min(32, len(data))]))
+            except Exception:
+                traceback.print_exc(file=sys.stderr)
+            finally:
+                debug2("[SocketRWShim] Thread 'stream_reader_to_sock' exiting")
+                self._s1.close()
+                self._on_end and self._on_end()
+
+        def stream_sock_to_writer():
+            try:
+                for data in iter(lambda: self._s1.recv(16384), b''):
+                    while data:
+                        n = self._w.write(data)
+                        data = data[n:]
+                    # debug3("[SocketRWShim] <<<<< w.write() %d %r..." % (len(data), data[:min(32, len(data))]))
+            except Exception:
+                traceback.print_exc(file=sys.stderr)
+            finally:
+                debug2("[SocketRWShim] Thread 'stream_sock_to_writer' exiting")
+                self._s1.close()
+                self._on_end and self._on_end()
+
+        self._t1 = threading.Thread(target=stream_reader_to_sock,  name='stream_reader_to_sock', daemon=True).start()
+        self._t2 = threading.Thread(target=stream_sock_to_writer, name='stream_sock_to_writer',  daemon=True).start()
+
+    def makefiles(self):
+        return self._s2.makefile("rb", buffering=0), self._s2.makefile("wb", buffering=0)
diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index b8b947a99..8c37fa229 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -12,7 +12,7 @@
 from urllib.parse import urlparse
 
 import sshuttle.helpers as helpers
-from sshuttle.helpers import debug2, which, get_path, Fatal
+from sshuttle.helpers import debug2, which, get_path, SocketRWShim, Fatal
 
 
 def get_module_source(name):
@@ -226,42 +226,14 @@ def get_server_io():
         #   Either to use sockets as stdio for subprocess. Or to use pipes but with a select() alternative
         #   https://stackoverflow.com/questions/4993119/redirect-io-of-process-to-windows-socket
 
-        (s1, s2) = socket.socketpair()
         pstdin = ssubprocess.PIPE
         pstdout = ssubprocess.PIPE
 
         preexec_fn = None
 
         def get_server_io():
-            import threading
-
-            def stream_stdout_to_sock():
-                try:
-                    fd = p.stdout.fileno()
-                    for data in iter(lambda: os.read(fd, 16384), b''):
-                        s1.sendall(data)
-                        # debug3("<<<<< p.stdout.read() %d %r...", len(data), data[:min(32, len(data))])
-
-                finally:
-                    debug2("Thread 'stream_stdout_to_sock' exiting")
-                    s1.close()
-                    p.terminate()
-
-            def stream_sock_to_stdin():
-                try:
-                    for data in iter(lambda: s1.recv(16384), b''):
-                        # debug3("<<<<< p.stdout.write() %d %r...", len(data), data[:min(32, len(data))])
-                        while data:
-                            n = p.stdin.write(data)
-                            data = data[n:]
-                finally:
-                    debug2("Thread 'stream_sock_to_stdin' exiting")
-                    s1.close()
-                    p.terminate()
-
-            threading.Thread(target=stream_stdout_to_sock,  name='stream_stdout_to_sock', daemon=True).start()
-            threading.Thread(target=stream_sock_to_stdin, name='stream_sock_to_stdin',  daemon=True).start()
-            return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0)
+            shim = SocketRWShim(p.stdout, p.stdin, on_end=lambda: p.terminate())
+            return shim.makefiles()
 
     # See: stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows
     close_fds = False if sys.platform == 'win32' else True

From 51287dc4dbbe21db4f2ab28e88718c4440f4ec5c Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Sun, 14 Apr 2024 14:09:17 +0530
Subject: [PATCH 205/275] support server on Windows

---
 sshuttle/helpers.py |  2 +-
 sshuttle/server.py  | 44 +++++++++++++++++++++++++++++++++++---------
 sshuttle/ssh.py     |  6 +++---
 sshuttle/ssnet.py   |  4 ++++
 4 files changed, 43 insertions(+), 13 deletions(-)

diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py
index c1f4baf58..85ff05769 100644
--- a/sshuttle/helpers.py
+++ b/sshuttle/helpers.py
@@ -281,7 +281,7 @@ def close(self):
 class SocketRWShim:
     __slots__ = ('_r', '_w', '_on_end', '_s1', '_s2', '_t1', '_t2')
 
-    def __init__(self, r, w, on_end=None) -> None:
+    def __init__(self, r, w, on_end=None):
         self._r = r
         self._w = w
         self._on_end = on_end
diff --git a/sshuttle/server.py b/sshuttle/server.py
index ddca1f356..62c156f45 100644
--- a/sshuttle/server.py
+++ b/sshuttle/server.py
@@ -14,7 +14,7 @@
 import subprocess as ssubprocess
 from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
 from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
-    resolvconf_random_nameserver, which, get_env
+    resolvconf_random_nameserver, which, get_env, SocketRWShim
 
 
 def _ipmatch(ipstr):
@@ -79,6 +79,20 @@ def _route_iproute(line):
     return ipw, int(mask)
 
 
+def _route_windows(line):
+    if " On-link " not in line:
+        return None, None
+    dest, net_mask = re.split(r'\s+', line.strip())[:2]
+    if net_mask == "255.255.255.255":
+        return None, None
+    for p in ('127.', '0.', '224.', '169.254.'):
+        if dest.startswith(p):
+            return None, None
+    ipw = _ipmatch(dest)
+    mask = _maskbits(_ipmatch(net_mask))
+    return ipw, mask
+
+
 def _list_routes(argv, extract_route):
     # FIXME: IPv4 only
     p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
@@ -101,14 +115,17 @@ def _list_routes(argv, extract_route):
 
 
 def list_routes():
-    if which('ip'):
-        routes = _list_routes(['ip', 'route'], _route_iproute)
-    elif which('netstat'):
-        routes = _list_routes(['netstat', '-rn'], _route_netstat)
+    if sys.platform == 'win32':
+        routes = _list_routes(['route', 'PRINT', '-4'], _route_windows)
     else:
-        log('WARNING: Neither "ip" nor "netstat" were found on the server. '
-            '--auto-nets feature will not work.')
-        routes = []
+        if which('ip'):
+            routes = _list_routes(['ip', 'route'], _route_iproute)
+        elif which('netstat'):
+            routes = _list_routes(['netstat', '-rn'], _route_netstat)
+        else:
+            log('WARNING: Neither "ip" nor "netstat" were found on the server. '
+                '--auto-nets feature will not work.')
+            routes = []
 
     for (family, ip, width) in routes:
         if not ip.startswith('0.') and not ip.startswith('127.'):
@@ -282,7 +299,16 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
         sys.stdout.flush()
 
         handlers = []
-        mux = Mux(io.FileIO(0, mode='r'), io.FileIO(1, mode='w'))
+        # get unbuffered stdin and stdout in binary mode. Equivalent to stdin.buffer/stdout.buffer (Only available in Python 3)
+        r, w = io.FileIO(0, mode='r'), io.FileIO(1, mode='w')
+        if sys.platform == 'win32':
+            def _deferred_exit():
+                time.sleep(1)  # give enough time to write logs to stderr
+                os._exit(23)
+            shim = SocketRWShim(r, w, on_end=_deferred_exit)
+            mux = Mux(*shim.makefiles())
+        else:
+            mux = Mux(r, w)
         handlers.append(mux)
 
         debug1('auto-nets:' + str(auto_nets))
diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index 8c37fa229..d7967c5bc 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options):
     pyscript = r"""
                 import sys, os;
                 verbosity=%d;
-                stdin = os.fdopen(0, "rb");
-                exec(compile(stdin.read(%d), "assembler.py", "exec"));
+                stdin = os.fdopen(0, 'rb');
+                exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
                 sys.exit(98);
                 """ % (helpers.verbose or 0, len(content))
     pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@@ -135,7 +135,7 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options):
         else:
             portl = []
         if python:
-            pycmd = "'%s' -c '%s'" % (python, pyscript)
+            pycmd = '"%s" -c "%s"' % (python, pyscript)
         else:
             # By default, we run the following code in a shell.
             # However, with restricted shells and other unusual
diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py
index 9438ce885..7b69cc869 100644
--- a/sshuttle/ssnet.py
+++ b/sshuttle/ssnet.py
@@ -168,6 +168,8 @@ def try_connect(self):
                 debug3('%r: fixed connect result: %s' % (self, e))
             if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
                 pass  # not connected yet
+            elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK:
+                pass  # not connected yet
             elif e.args[0] == 0:
                 # connected successfully (weird Linux bug?)
                 # Sometimes Linux seems to return EINVAL when it isn't
@@ -382,11 +384,13 @@ def send(self, channel, cmd, data):
         debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
                % (channel, cmd_to_name.get(cmd, hex(cmd)),
                   len(data), self.fullness))
+        # debug3('>>> data: %r' % data)
         self.fullness += len(data)
 
     def got_packet(self, channel, cmd, data):
         debug2('<  channel=%d cmd=%s len=%d'
                % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
+        # debug3('<<< data: %r' % data)
         if cmd == CMD_PING:
             self.send(0, CMD_PONG, data)
         elif cmd == CMD_PONG:

From b826ae6b910ba71b95d42cbda4f8128e8681e60d Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Wed, 1 May 2024 20:08:15 +0530
Subject: [PATCH 206/275] windows: support automatic nameserver detection for
 --dns option

---
 sshuttle/helpers.py          | 43 +++++++++++++++++++++++++++++-------
 sshuttle/server.py           |  4 ++--
 tests/client/test_helpers.py |  4 ++--
 3 files changed, 39 insertions(+), 12 deletions(-)

diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py
index 85ff05769..2c989400e 100644
--- a/sshuttle/helpers.py
+++ b/sshuttle/helpers.py
@@ -3,7 +3,9 @@
 import errno
 import os
 import threading
+import subprocess
 import traceback
+import re
 
 if sys.platform != "win32":
     import fcntl
@@ -114,18 +116,43 @@ def resolvconf_nameservers(systemd_resolved):
     return nsservers
 
 
-def resolvconf_random_nameserver(systemd_resolved):
+def windows_nameservers():
+    out = subprocess.check_output(["powershell", "-NonInteractive", "-NoProfile", "-Command", "Get-DnsClientServerAddress"],
+                                  encoding="utf-8")
+    servers = set()
+    for line in out.splitlines():
+        if line.startswith("Loopback "):
+            continue
+        m = re.search(r'{.+}', line)
+        if not m:
+            continue
+        for s in m.group().strip('{}').split(','):
+            s = s.strip()
+            if s.startswith('fec0:0:0:ffff'):
+                continue
+            servers.add(s)
+    debug2("Found DNS servers: %s" % servers)
+    return [(socket.AF_INET6 if ':' in s else socket.AF_INET, s) for s in servers]
+
+
+def get_random_nameserver():
     """Return a random nameserver selected from servers produced by
-    resolvconf_nameservers(). See documentation for
-    resolvconf_nameservers() for a description of the parameter.
+    resolvconf_nameservers()/windows_nameservers()
     """
-    lines = resolvconf_nameservers(systemd_resolved)
-    if lines:
-        if len(lines) > 1:
+    if sys.platform == "win32":
+        if globals().get('_nameservers') is None:
+            ns_list = windows_nameservers()
+            globals()['_nameservers'] = ns_list
+        else:
+            ns_list = globals()['_nameservers']
+    else:
+        ns_list = resolvconf_nameservers(systemd_resolved=False)
+    if ns_list:
+        if len(ns_list) > 1:
             # don't import this unless we really need it
             import random
-            random.shuffle(lines)
-        return lines[0]
+            random.shuffle(ns_list)
+        return ns_list[0]
     else:
         return (socket.AF_INET, '127.0.0.1')
 
diff --git a/sshuttle/server.py b/sshuttle/server.py
index 62c156f45..4350dfb7d 100644
--- a/sshuttle/server.py
+++ b/sshuttle/server.py
@@ -14,7 +14,7 @@
 import subprocess as ssubprocess
 from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
 from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
-    resolvconf_random_nameserver, which, get_env, SocketRWShim
+    get_random_nameserver, which, get_env, SocketRWShim
 
 
 def _ipmatch(ipstr):
@@ -199,7 +199,7 @@ def try_send(self):
         self.tries += 1
 
         if self.to_nameserver is None:
-            _, peer = resolvconf_random_nameserver(False)
+            _, peer = get_random_nameserver()
             port = 53
         else:
             peer = self.to_ns_peer
diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py
index ca1aba334..bfbb145ab 100644
--- a/tests/client/test_helpers.py
+++ b/tests/client/test_helpers.py
@@ -143,7 +143,7 @@ def test_resolvconf_nameservers(mock_open):
 
 
 @patch('sshuttle.helpers.open', create=True)
-def test_resolvconf_random_nameserver(mock_open):
+def test_get_random_nameserver(mock_open):
     mock_open.return_value = io.StringIO(u"""
 # Generated by NetworkManager
 search pri
@@ -156,7 +156,7 @@ def test_resolvconf_random_nameserver(mock_open):
 nameserver 2404:6800:4004:80c::3
 nameserver 2404:6800:4004:80c::4
 """)
-    ns = sshuttle.helpers.resolvconf_random_nameserver(False)
+    ns = sshuttle.helpers.get_random_nameserver()
     assert ns in [
         (AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
         (AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),

From 554b8e3ae52712d8e60645d651ca68bad86ffb4f Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Wed, 1 May 2024 20:08:49 +0530
Subject: [PATCH 207/275] windows: improve ssnet/try_connect() logic

---
 sshuttle/ssnet.py | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py
index 7b69cc869..6c32a92d1 100644
--- a/sshuttle/ssnet.py
+++ b/sshuttle/ssnet.py
@@ -168,21 +168,25 @@ def try_connect(self):
                 debug3('%r: fixed connect result: %s' % (self, e))
             if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
                 pass  # not connected yet
-            elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK:
+            elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK:  # 10035
                 pass  # not connected yet
             elif e.args[0] == 0:
-                # connected successfully (weird Linux bug?)
-                # Sometimes Linux seems to return EINVAL when it isn't
-                # invalid.  This *may* be caused by a race condition
-                # between connect() and getsockopt(SO_ERROR) (ie. it
-                # finishes connecting in between the two, so there is no
-                # longer an error).  However, I'm not sure of that.
-                #
-                # I did get at least one report that the problem went away
-                # when we added this, however.
-                self.connect_to = None
+                if sys.platform == 'win32':
+                    # On Windows "real" error of EINVAL could be 0, when socket is in connecting state
+                    pass
+                else:
+                    # connected successfully (weird Linux bug?)
+                    # Sometimes Linux seems to return EINVAL when it isn't
+                    # invalid.  This *may* be caused by a race condition
+                    # between connect() and getsockopt(SO_ERROR) (ie. it
+                    # finishes connecting in between the two, so there is no
+                    # longer an error).  However, I'm not sure of that.
+                    #
+                    # I did get at least one report that the problem went away
+                    # when we added this, however.
+                    self.connect_to = None
             elif e.args[0] == errno.EISCONN:
-                # connected successfully (BSD)
+                # connected successfully (BSD + Windows)
                 self.connect_to = None
             elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
                 # a "normal" kind of error

From df9625bbfd37f1a19f653b8cc47ddece116e3987 Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Tue, 16 Jul 2024 21:34:33 +0530
Subject: [PATCH 208/275] windows: ignore netstat output encoding errors

---
 sshuttle/methods/windivert.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py
index fa61709c0..e6603e369 100644
--- a/sshuttle/methods/windivert.py
+++ b/sshuttle/methods/windivert.py
@@ -294,7 +294,7 @@ def __init__(self, name):
 
     def _get_bind_address_for_port(self, port, family):
         proto = "TCPv6" if family.version == 6 else "TCP"
-        for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines():
+        for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode(errors='ignore').splitlines():
             try:
                 _, local_addr, _, state, *_ = re.split(r"\s+", line.strip())
             except ValueError:

From dff6950c4c178b7612555bf6f9c02d0f397f4bc4 Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Tue, 16 Jul 2024 21:47:37 +0530
Subject: [PATCH 209/275] windows: update docs

---
 README.rst       | 6 +++++-
 docs/windows.rst | 9 ++++++---
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/README.rst b/README.rst
index 47a14f990..d268ed2da 100644
--- a/README.rst
+++ b/README.rst
@@ -4,7 +4,7 @@ sshuttle: where transparent proxy meets VPN meets ssh
 As far as I know, sshuttle is the only program that solves the following
 common case:
 
-- Your client machine (or router) is Linux, FreeBSD, or MacOS.
+- Your client machine (or router) is Linux, FreeBSD, MacOS or Windows.
 
 - You have access to a remote network via ssh.
 
@@ -108,6 +108,10 @@ It is also possible to install into a virtualenv as a non-root user.
 
       nix-env -iA nixpkgs.sshuttle
 
+- Windows::
+Use PyPI 
+
+      pip install sshuttle
 
 Documentation
 -------------
diff --git a/docs/windows.rst b/docs/windows.rst
index 2561cd390..d7462902f 100644
--- a/docs/windows.rst
+++ b/docs/windows.rst
@@ -1,11 +1,14 @@
 Microsoft Windows
 =================
 
-Experimental support::
+Experimental native support::
 
-Experimental built-in support for Windows is availble through `windivert` method.
-You have to install https://pypi.org/project/pydivert pacakge.  You need Administrator privileges to use windivert method
+Experimental built-in support for Windows is available through `windivert` method.
+You have to install https://pypi.org/project/pydivert package.  You need Administrator privileges to use windivert method
 
+Notes
+-  sshuttle should be executed from admin shell (Automatic firewall process admin elevation is not available)
+-  TCP/IPv4 supported (IPv6/UDP/DNS are not available)
 
 Use Linux VM on Windows::
 

From bac2a6b0c7945241f493e67e9329a13378daee7e Mon Sep 17 00:00:00 2001
From: nom3ad <19239479+nom3ad@users.noreply.github.com>
Date: Tue, 16 Jul 2024 23:38:36 +0530
Subject: [PATCH 210/275] windows: add --remote-shell option to select
 cmd/powershell

---
 docs/manpage.rst    |   5 +++
 sshuttle/client.py  |  11 ++---
 sshuttle/cmdline.py |   1 +
 sshuttle/options.py |   8 ++++
 sshuttle/ssh.py     | 101 +++++++++++++++++++++++---------------------
 5 files changed, 74 insertions(+), 52 deletions(-)

diff --git a/docs/manpage.rst b/docs/manpage.rst
index b860dde4b..49022ec86 100644
--- a/docs/manpage.rst
+++ b/docs/manpage.rst
@@ -181,6 +181,11 @@ Options
     in a non-standard location or you want to provide extra
     options to the ssh command, for example, ``-e 'ssh -v'``.
 
+.. option:: --remote-shell
+
+    For Windows targets, specify configured remote shell program alternative to defacto posix shell.
+    It would be either ``cmd`` or ``powershell`` unless something like git-bash is in use.
+
 .. option:: --no-cmd-delimiter
 
     Do not add a double dash (--) delimiter before invoking Python on
diff --git a/sshuttle/client.py b/sshuttle/client.py
index 8271cb903..08df6787a 100644
--- a/sshuttle/client.py
+++ b/sshuttle/client.py
@@ -211,8 +211,8 @@ def __init__(self, method_name, sudo_pythonpath):
         self.auto_nets = []
 
         argv0 = sys.argv[0]
-        # argv0 is either be a normal python file or an executable.
-        # After installed as a package, sshuttle command points to an .exe in Windows and python shebang script elsewhere.
+        # argv0 is either be a normal Python file or an executable.
+        # After installed as a package, sshuttle command points to an .exe in Windows and Python shebang script elsewhere.
         argvbase = (([sys.executable, sys.argv[0]] if argv0.endswith('.py') else [argv0]) +
                     ['-v'] * (helpers.verbose or 0) +
                     ['--method', method_name] +
@@ -591,7 +591,7 @@ def ondns(listener, method, mux, handlers):
 def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
           python, latency_control, latency_buffer_size,
           dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
-          to_nameserver, add_cmd_delimiter):
+          to_nameserver, add_cmd_delimiter, remote_shell):
 
     helpers.logprefix = 'c : '
     debug1('Starting client with Python version %s'
@@ -607,6 +607,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
             ssh_cmd, remotename, python,
             stderr=ssyslog._p and ssyslog._p.stdin,
             add_cmd_delimiter=add_cmd_delimiter,
+            remote_shell=remote_shell,
             options=dict(latency_control=latency_control,
                          latency_buffer_size=latency_buffer_size,
                          auto_hosts=auto_hosts,
@@ -809,7 +810,7 @@ def main(listenip_v6, listenip_v4,
          latency_buffer_size, dns, nslist,
          method_name, seed_hosts, auto_hosts, auto_nets,
          subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
-         user, group, sudo_pythonpath, add_cmd_delimiter, tmark):
+         user, group, sudo_pythonpath, add_cmd_delimiter, remote_shell, tmark):
 
     if not remotename:
         raise Fatal("You must use -r/--remote to specify a remote "
@@ -1158,7 +1159,7 @@ def feature_status(label, enabled, available):
         return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
                      python, latency_control, latency_buffer_size,
                      dns_listener, seed_hosts, auto_hosts, auto_nets,
-                     daemon, to_nameserver, add_cmd_delimiter)
+                     daemon, to_nameserver, add_cmd_delimiter, remote_shell)
     finally:
         try:
             if daemon:
diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py
index 3e98eee2e..11d679642 100644
--- a/sshuttle/cmdline.py
+++ b/sshuttle/cmdline.py
@@ -116,6 +116,7 @@ def main():
                                       opt.group,
                                       opt.sudo_pythonpath,
                                       opt.add_cmd_delimiter,
+                                      opt.remote_shell,
                                       opt.tmark)
 
             if return_code == 0:
diff --git a/sshuttle/options.py b/sshuttle/options.py
index ac5a96da0..b610a11dc 100644
--- a/sshuttle/options.py
+++ b/sshuttle/options.py
@@ -315,6 +315,14 @@ def convert_arg_line_to_args(self, arg_line):
     do not add a double dash before the python command
     """
 )
+parser.add_argument(
+    "--remote-shell",
+    metavar="PROGRAM",
+    help="""
+    alternate remote shell program instead of defacto posix shell.
+    For Windows targets it would be either `cmd` or `powershell` unless something like git-bash is in use.
+    """
+)
 parser.add_argument(
     "--seed-hosts",
     metavar="HOSTNAME[,HOSTNAME]",
diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index d7967c5bc..8f295e214 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -84,7 +84,7 @@ def parse_hostport(rhostport):
     return username, password, port, host
 
 
-def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options):
+def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell, options):
     username, password, port, host = parse_hostport(rhostport)
     if username:
         rhost = "{}@{}".format(username, host)
@@ -134,52 +134,59 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, options):
             portl = ["-p", str(port)]
         else:
             portl = []
-        if python:
-            pycmd = '"%s" -c "%s"' % (python, pyscript)
-        else:
-            # By default, we run the following code in a shell.
-            # However, with restricted shells and other unusual
-            # situations, there can be trouble. See the RESTRICTED
-            # SHELL section in "man bash" for more information. The
-            # code makes many assumptions:
-            #
-            # (1) That /bin/sh exists and that we can call it.
-            # Restricted shells often do *not* allow you to run
-            # programs specified with an absolute path like /bin/sh.
-            # Either way, if there is trouble with this, it should
-            # return error code 127.
-            #
-            # (2) python3 or python exists in the PATH and is
-            # executable. If they aren't, then exec won't work (see (4)
-            # below).
-            #
-            # (3) In /bin/sh, that we can redirect stderr in order to
-            # hide the version that "python3 -V" might print (some
-            # restricted shells don't allow redirection, see
-            # RESTRICTED SHELL section in 'man bash'). However, if we
-            # are in a restricted shell, we'd likely have trouble with
-            # assumption (1) above.
-            #
-            # (4) The 'exec' command should work except if we failed
-            # to exec python because it doesn't exist or isn't
-            # executable OR if exec isn't allowed (some restricted
-            # shells don't allow exec). If the exec succeeded, it will
-            # not return and not get to the "exit 97" command. If exec
-            # does return, we exit with code 97.
-            #
-            # Specifying the exact python program to run with --python
-            # avoids many of the issues above. However, if
-            # you have a restricted shell on remote, you may only be
-            # able to run python if it is in your PATH (and you can't
-            # run programs specified with an absolute path). In that
-            # case, sshuttle might not work at all since it is not
-            # possible to run python on the remote machine---even if
-            # it is present.
-            devnull = '/dev/null'
-            pycmd = ("P=python3; $P -V 2>%s || P=python; "
-                     "exec \"$P\" -c %s; exit 97") % \
-                (devnull, quote(pyscript))
-            pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
+        if remote_shell == "cmd":
+            pycmd = '"%s" -c "%s"' % (python or 'python', pyscript)
+        elif remote_shell == "powershell":
+            for c in ('\'', ' ', ';', '(', ')', ','):
+                pyscript = pyscript.replace(c, '`' + c)
+            pycmd = '%s -c %s' % (python or 'python', pyscript)
+        else:  # posix shell expected
+            if python:
+                pycmd = '"%s" -c "%s"' % (python, pyscript)
+            else:
+                # By default, we run the following code in a shell.
+                # However, with restricted shells and other unusual
+                # situations, there can be trouble. See the RESTRICTED
+                # SHELL section in "man bash" for more information. The
+                # code makes many assumptions:
+                #
+                # (1) That /bin/sh exists and that we can call it.
+                # Restricted shells often do *not* allow you to run
+                # programs specified with an absolute path like /bin/sh.
+                # Either way, if there is trouble with this, it should
+                # return error code 127.
+                #
+                # (2) python3 or python exists in the PATH and is
+                # executable. If they aren't, then exec won't work (see (4)
+                # below).
+                #
+                # (3) In /bin/sh, that we can redirect stderr in order to
+                # hide the version that "python3 -V" might print (some
+                # restricted shells don't allow redirection, see
+                # RESTRICTED SHELL section in 'man bash'). However, if we
+                # are in a restricted shell, we'd likely have trouble with
+                # assumption (1) above.
+                #
+                # (4) The 'exec' command should work except if we failed
+                # to exec python because it doesn't exist or isn't
+                # executable OR if exec isn't allowed (some restricted
+                # shells don't allow exec). If the exec succeeded, it will
+                # not return and not get to the "exit 97" command. If exec
+                # does return, we exit with code 97.
+                #
+                # Specifying the exact python program to run with --python
+                # avoids many of the issues above. However, if
+                # you have a restricted shell on remote, you may only be
+                # able to run python if it is in your PATH (and you can't
+                # run programs specified with an absolute path). In that
+                # case, sshuttle might not work at all since it is not
+                # possible to run python on the remote machine---even if
+                # it is present.
+                devnull = '/dev/null'
+                pycmd = ("P=python3; $P -V 2>%s || P=python; "
+                         "exec \"$P\" -c %s; exit 97") % \
+                    (devnull, quote(pyscript))
+                pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
 
         if password is not None:
             os.environ['SSHPASS'] = str(password)

From b9e7a8071545677a48e6e035921a31741d183a3e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 6 Aug 2024 10:54:59 +0000
Subject: [PATCH 211/275] Bump furo from 2024.7.18 to 2024.8.6

Bumps [furo](https://github.com/pradyunsg/furo) from 2024.7.18 to 2024.8.6.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.07.18...2024.08.06)

---
updated-dependencies:
- dependency-name: furo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 0489d9256..fa703fb83 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 Sphinx==7.1.2
-furo==2024.7.18
+furo==2024.8.6

From 60ee5b910b8b59037a1329a37915df36c321f9a3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 6 Aug 2024 10:55:19 +0000
Subject: [PATCH 212/275] Bump flake8 from 7.1.0 to 7.1.1

Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.0 to 7.1.1.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.0...7.1.1)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 4ea29c1dd..b0b231889 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -344,13 +344,13 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "flake8"
-version = "7.1.0"
+version = "7.1.1"
 description = "the modular source code checker: pep8 pyflakes and co"
 optional = false
 python-versions = ">=3.8.1"
 files = [
-    {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"},
-    {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"},
+    {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
+    {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
 ]
 
 [package.dependencies]

From 8da94c39eabe4207eaa0843ce93b868d65f7733f Mon Sep 17 00:00:00 2001
From: Nico T 
Date: Wed, 3 Jan 2024 13:03:45 -0500
Subject: [PATCH 213/275] transfer work from PR #837

---
 sshuttle/cmdline.py          |  2 +-
 sshuttle/hostwatch.py        | 10 +++++++++-
 tests/client/test_helpers.py |  1 +
 3 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py
index 11d679642..a32c6e79b 100644
--- a/sshuttle/cmdline.py
+++ b/sshuttle/cmdline.py
@@ -9,7 +9,7 @@
 import sshuttle.hostwatch as hostwatch
 import sshuttle.ssyslog as ssyslog
 from sshuttle.options import parser, parse_ipport
-from sshuttle.helpers import family_ip_tuple, log, Fatal
+from sshuttle.helpers import family_ip_tuple, log, Fatal, start_stdout_stderr_flush_thread
 from sshuttle.sudoers import sudoers
 
 
diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py
index 35ab2cc95..1884165ca 100644
--- a/sshuttle/hostwatch.py
+++ b/sshuttle/hostwatch.py
@@ -18,6 +18,8 @@
 # Have we already failed to write CACHEFILE?
 CACHE_WRITE_FAILED = False
 
+SHOULD_WRITE_CACHE = False
+
 hostnames = {}
 queue = {}
 try:
@@ -81,6 +83,11 @@ def read_host_cache():
             ip = re.sub(r'[^0-9.]', '', ip).strip()
             if name and ip:
                 found_host(name, ip)
+    f.close()
+    global SHOULD_WRITE_CACHE
+    if SHOULD_WRITE_CACHE:
+        write_host_cache()
+        SHOULD_WRITE_CACHE = False
 
 
 def found_host(name, ip):
@@ -97,12 +104,13 @@ def found_host(name, ip):
     if hostname != name:
         found_host(hostname, ip)
 
+    global SHOULD_WRITE_CACHE
     oldip = hostnames.get(name)
     if oldip != ip:
         hostnames[name] = ip
         debug1('Found: %s: %s' % (name, ip))
         sys.stdout.write('%s,%s\n' % (name, ip))
-        write_host_cache()
+        SHOULD_WRITE_CACHE = True
 
 
 def _check_etc_hosts():
diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py
index bfbb145ab..794c284d3 100644
--- a/tests/client/test_helpers.py
+++ b/tests/client/test_helpers.py
@@ -2,6 +2,7 @@
 import socket
 from socket import AF_INET, AF_INET6
 import errno
+import time
 
 from unittest.mock import patch, call
 import sshuttle.helpers

From 8364fd96e8ba3b082ffb0d7dda21a3236a66eab5 Mon Sep 17 00:00:00 2001
From: nicole trinity 
Date: Wed, 7 Aug 2024 10:33:25 -0400
Subject: [PATCH 214/275] remove unused imports

---
 sshuttle/cmdline.py          | 2 +-
 tests/client/test_helpers.py | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py
index a32c6e79b..11d679642 100644
--- a/sshuttle/cmdline.py
+++ b/sshuttle/cmdline.py
@@ -9,7 +9,7 @@
 import sshuttle.hostwatch as hostwatch
 import sshuttle.ssyslog as ssyslog
 from sshuttle.options import parser, parse_ipport
-from sshuttle.helpers import family_ip_tuple, log, Fatal, start_stdout_stderr_flush_thread
+from sshuttle.helpers import family_ip_tuple, log, Fatal
 from sshuttle.sudoers import sudoers
 
 
diff --git a/tests/client/test_helpers.py b/tests/client/test_helpers.py
index 794c284d3..bfbb145ab 100644
--- a/tests/client/test_helpers.py
+++ b/tests/client/test_helpers.py
@@ -2,7 +2,6 @@
 import socket
 from socket import AF_INET, AF_INET6
 import errno
-import time
 
 from unittest.mock import patch, call
 import sshuttle.helpers

From 6272a0212ccb14b1b32d974e0008cd9e49d526de Mon Sep 17 00:00:00 2001
From: o2 
Date: Tue, 13 Aug 2024 17:13:07 +0900
Subject: [PATCH 215/275] Suppress error P is not recognized as an internal or
 external command,operable program or batch file.

---
 sshuttle/ssh.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index 8f295e214..d4451f56b 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
     pyscript = r"""
                 import sys, os;
                 verbosity=%d;
-                stdin = os.fdopen(0, 'rb');
-                exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
+                stdin = os.fdopen(0, "rb");
+                exec(compile(stdin.read(%d), "assembler.py", "exec"));
                 sys.exit(98);
                 """ % (helpers.verbose or 0, len(content))
     pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@@ -142,7 +142,7 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
             pycmd = '%s -c %s' % (python or 'python', pyscript)
         else:  # posix shell expected
             if python:
-                pycmd = '"%s" -c "%s"' % (python, pyscript)
+                pycmd = "%s -c \"%s\"" % (python, pyscript)
             else:
                 # By default, we run the following code in a shell.
                 # However, with restricted shells and other unusual

From dc2287ccf851b27ca556ebccbf3fb241d81faf99 Mon Sep 17 00:00:00 2001
From: o2 
Date: Tue, 13 Aug 2024 17:20:12 +0900
Subject: [PATCH 216/275] add next error log

---
 not_a_socket_error.log | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
 create mode 100644 not_a_socket_error.log

diff --git a/not_a_socket_error.log b/not_a_socket_error.log
new file mode 100644
index 000000000..d5e538c42
--- /dev/null
+++ b/not_a_socket_error.log
@@ -0,0 +1,29 @@
+c : executing: ['/usr/bin/sshpass', '-p', '*****', 'ssh', '-p', '*****', '****@*************', '--', 'python -c "import sys, os; verbosity=2; sys.stdin = os.fdopen(0, \'rb\'); exec(compile(sys.stdin.read(1702), \'assembler.py\', \'exec\')); sys.exit(98);"']
+c :  > channel=0 cmd=PING len=7 (fullness=0)
+sign_and_send_pubkey: no mutual signature supported
+ s: Running server on remote host with C:\Python312\python.exe (version 3.12.4)
+ s: assembling 'sshuttle' (88 bytes)
+ s: assembling 'sshuttle.cmdline_options' (85 bytes)
+ s: assembling 'sshuttle.helpers' (2655 bytes)
+ s: assembling 'sshuttle.ssnet' (5717 bytes)
+ s: assembling 'sshuttle.hostwatch' (2516 bytes)
+ s: assembling 'sshuttle.server' (3781 bytes)
+ s: latency control setting = True
+ s:  > channel=0 cmd=PING len=7 (fullness=0)
+ s: auto-nets:False
+ s:  > channel=0 cmd=ROUTES len=0 (fullness=7)
+c : Connected to server.
+c : Waiting: 3 r=[5, 7, 8, 9, 11] w=[11] x=[] (fullness=7/0)
+c :   Ready: 3 r=[5, 7, 8, 9, 11] w=[11] x=[]
+ s: Waiting: 1 r=[0] w=[1] x=[] (fullness=7/0)
+ s:   Ready: 1 r=[0] w=[1] x=[]
+r2
+c : mux wrote: 15/15
+r2
+Traceback (most recent call last):
+  File "", line 1, in 
+  File "assembler.py", line 47, in 
+  File "sshuttle.server", line 389, in main
+  File "sshuttle.ssnet", line 610, in runonce
+  File "sshuttle.ssnet", line 496, in callback
+OSError: [WinError 10038] An operation was attempted on something that is not a socket

From d1dbed04a0e886945c6391c7ba6a2abee6336fa4 Mon Sep 17 00:00:00 2001
From: o2 
Date: Tue, 13 Aug 2024 19:08:57 +0900
Subject: [PATCH 217/275] restore single quote then it looks working

---
 sshuttle/ssh.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index d4451f56b..82406310f 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
     pyscript = r"""
                 import sys, os;
                 verbosity=%d;
-                stdin = os.fdopen(0, "rb");
-                exec(compile(stdin.read(%d), "assembler.py", "exec"));
+                stdin = os.fdopen(0, 'rb');
+                exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
                 sys.exit(98);
                 """ % (helpers.verbose or 0, len(content))
     pyscript = re.sub(r'\s+', ' ', pyscript.strip())

From eaf55ed2960234b44df02135b2bb381a8c59e66e Mon Sep 17 00:00:00 2001
From: o2 
Date: Tue, 13 Aug 2024 21:14:31 +0900
Subject: [PATCH 218/275] remove unnecessary log file

---
 not_a_socket_error.log | 29 -----------------------------
 1 file changed, 29 deletions(-)
 delete mode 100644 not_a_socket_error.log

diff --git a/not_a_socket_error.log b/not_a_socket_error.log
deleted file mode 100644
index d5e538c42..000000000
--- a/not_a_socket_error.log
+++ /dev/null
@@ -1,29 +0,0 @@
-c : executing: ['/usr/bin/sshpass', '-p', '*****', 'ssh', '-p', '*****', '****@*************', '--', 'python -c "import sys, os; verbosity=2; sys.stdin = os.fdopen(0, \'rb\'); exec(compile(sys.stdin.read(1702), \'assembler.py\', \'exec\')); sys.exit(98);"']
-c :  > channel=0 cmd=PING len=7 (fullness=0)
-sign_and_send_pubkey: no mutual signature supported
- s: Running server on remote host with C:\Python312\python.exe (version 3.12.4)
- s: assembling 'sshuttle' (88 bytes)
- s: assembling 'sshuttle.cmdline_options' (85 bytes)
- s: assembling 'sshuttle.helpers' (2655 bytes)
- s: assembling 'sshuttle.ssnet' (5717 bytes)
- s: assembling 'sshuttle.hostwatch' (2516 bytes)
- s: assembling 'sshuttle.server' (3781 bytes)
- s: latency control setting = True
- s:  > channel=0 cmd=PING len=7 (fullness=0)
- s: auto-nets:False
- s:  > channel=0 cmd=ROUTES len=0 (fullness=7)
-c : Connected to server.
-c : Waiting: 3 r=[5, 7, 8, 9, 11] w=[11] x=[] (fullness=7/0)
-c :   Ready: 3 r=[5, 7, 8, 9, 11] w=[11] x=[]
- s: Waiting: 1 r=[0] w=[1] x=[] (fullness=7/0)
- s:   Ready: 1 r=[0] w=[1] x=[]
-r2
-c : mux wrote: 15/15
-r2
-Traceback (most recent call last):
-  File "", line 1, in 
-  File "assembler.py", line 47, in 
-  File "sshuttle.server", line 389, in main
-  File "sshuttle.ssnet", line 610, in runonce
-  File "sshuttle.ssnet", line 496, in callback
-OSError: [WinError 10038] An operation was attempted on something that is not a socket

From 81532b29a91d617513f7f0135bfbc590dc7c4c07 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 20 Aug 2024 16:23:43 +1000
Subject: [PATCH 219/275] Revert "remove unnecessary log file"

This reverts commit eaf55ed2960234b44df02135b2bb381a8c59e66e.
---
 not_a_socket_error.log | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
 create mode 100644 not_a_socket_error.log

diff --git a/not_a_socket_error.log b/not_a_socket_error.log
new file mode 100644
index 000000000..d5e538c42
--- /dev/null
+++ b/not_a_socket_error.log
@@ -0,0 +1,29 @@
+c : executing: ['/usr/bin/sshpass', '-p', '*****', 'ssh', '-p', '*****', '****@*************', '--', 'python -c "import sys, os; verbosity=2; sys.stdin = os.fdopen(0, \'rb\'); exec(compile(sys.stdin.read(1702), \'assembler.py\', \'exec\')); sys.exit(98);"']
+c :  > channel=0 cmd=PING len=7 (fullness=0)
+sign_and_send_pubkey: no mutual signature supported
+ s: Running server on remote host with C:\Python312\python.exe (version 3.12.4)
+ s: assembling 'sshuttle' (88 bytes)
+ s: assembling 'sshuttle.cmdline_options' (85 bytes)
+ s: assembling 'sshuttle.helpers' (2655 bytes)
+ s: assembling 'sshuttle.ssnet' (5717 bytes)
+ s: assembling 'sshuttle.hostwatch' (2516 bytes)
+ s: assembling 'sshuttle.server' (3781 bytes)
+ s: latency control setting = True
+ s:  > channel=0 cmd=PING len=7 (fullness=0)
+ s: auto-nets:False
+ s:  > channel=0 cmd=ROUTES len=0 (fullness=7)
+c : Connected to server.
+c : Waiting: 3 r=[5, 7, 8, 9, 11] w=[11] x=[] (fullness=7/0)
+c :   Ready: 3 r=[5, 7, 8, 9, 11] w=[11] x=[]
+ s: Waiting: 1 r=[0] w=[1] x=[] (fullness=7/0)
+ s:   Ready: 1 r=[0] w=[1] x=[]
+r2
+c : mux wrote: 15/15
+r2
+Traceback (most recent call last):
+  File "", line 1, in 
+  File "assembler.py", line 47, in 
+  File "sshuttle.server", line 389, in main
+  File "sshuttle.ssnet", line 610, in runonce
+  File "sshuttle.ssnet", line 496, in callback
+OSError: [WinError 10038] An operation was attempted on something that is not a socket

From 09c3324978f5f7ddeec36f3ecb52f2d66f9546d9 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 20 Aug 2024 16:23:43 +1000
Subject: [PATCH 220/275] Revert "restore single quote then it looks working"

This reverts commit d1dbed04a0e886945c6391c7ba6a2abee6336fa4.
---
 sshuttle/ssh.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index 82406310f..d4451f56b 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
     pyscript = r"""
                 import sys, os;
                 verbosity=%d;
-                stdin = os.fdopen(0, 'rb');
-                exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
+                stdin = os.fdopen(0, "rb");
+                exec(compile(stdin.read(%d), "assembler.py", "exec"));
                 sys.exit(98);
                 """ % (helpers.verbose or 0, len(content))
     pyscript = re.sub(r'\s+', ' ', pyscript.strip())

From ac36a8a20e52b25a55c6ec7fbd3bdc50df47feef Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 20 Aug 2024 16:23:43 +1000
Subject: [PATCH 221/275] Revert "add next error log"

This reverts commit dc2287ccf851b27ca556ebccbf3fb241d81faf99.
---
 not_a_socket_error.log | 29 -----------------------------
 1 file changed, 29 deletions(-)
 delete mode 100644 not_a_socket_error.log

diff --git a/not_a_socket_error.log b/not_a_socket_error.log
deleted file mode 100644
index d5e538c42..000000000
--- a/not_a_socket_error.log
+++ /dev/null
@@ -1,29 +0,0 @@
-c : executing: ['/usr/bin/sshpass', '-p', '*****', 'ssh', '-p', '*****', '****@*************', '--', 'python -c "import sys, os; verbosity=2; sys.stdin = os.fdopen(0, \'rb\'); exec(compile(sys.stdin.read(1702), \'assembler.py\', \'exec\')); sys.exit(98);"']
-c :  > channel=0 cmd=PING len=7 (fullness=0)
-sign_and_send_pubkey: no mutual signature supported
- s: Running server on remote host with C:\Python312\python.exe (version 3.12.4)
- s: assembling 'sshuttle' (88 bytes)
- s: assembling 'sshuttle.cmdline_options' (85 bytes)
- s: assembling 'sshuttle.helpers' (2655 bytes)
- s: assembling 'sshuttle.ssnet' (5717 bytes)
- s: assembling 'sshuttle.hostwatch' (2516 bytes)
- s: assembling 'sshuttle.server' (3781 bytes)
- s: latency control setting = True
- s:  > channel=0 cmd=PING len=7 (fullness=0)
- s: auto-nets:False
- s:  > channel=0 cmd=ROUTES len=0 (fullness=7)
-c : Connected to server.
-c : Waiting: 3 r=[5, 7, 8, 9, 11] w=[11] x=[] (fullness=7/0)
-c :   Ready: 3 r=[5, 7, 8, 9, 11] w=[11] x=[]
- s: Waiting: 1 r=[0] w=[1] x=[] (fullness=7/0)
- s:   Ready: 1 r=[0] w=[1] x=[]
-r2
-c : mux wrote: 15/15
-r2
-Traceback (most recent call last):
-  File "", line 1, in 
-  File "assembler.py", line 47, in 
-  File "sshuttle.server", line 389, in main
-  File "sshuttle.ssnet", line 610, in runonce
-  File "sshuttle.ssnet", line 496, in callback
-OSError: [WinError 10038] An operation was attempted on something that is not a socket

From e6074ed52d5c26f3cf53ffd04219f9fec32d2aed Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 20 Aug 2024 16:23:43 +1000
Subject: [PATCH 222/275] Revert "Suppress error P is not recognized as an
 internal or external command,operable program or batch file."

This reverts commit 6272a0212ccb14b1b32d974e0008cd9e49d526de.
---
 sshuttle/ssh.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index d4451f56b..8f295e214 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
     pyscript = r"""
                 import sys, os;
                 verbosity=%d;
-                stdin = os.fdopen(0, "rb");
-                exec(compile(stdin.read(%d), "assembler.py", "exec"));
+                stdin = os.fdopen(0, 'rb');
+                exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
                 sys.exit(98);
                 """ % (helpers.verbose or 0, len(content))
     pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@@ -142,7 +142,7 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
             pycmd = '%s -c %s' % (python or 'python', pyscript)
         else:  # posix shell expected
             if python:
-                pycmd = "%s -c \"%s\"" % (python, pyscript)
+                pycmd = '"%s" -c "%s"' % (python, pyscript)
             else:
                 # By default, we run the following code in a shell.
                 # However, with restricted shells and other unusual

From f05d6531f2b0ece31f237b436d440fd69618aabd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Sep 2024 10:32:37 +0000
Subject: [PATCH 223/275] Bump pytest from 8.3.2 to 8.3.3

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-tests.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-tests.txt b/requirements-tests.txt
index 48661fb34..68b83b327 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,5 +1,5 @@
 -r requirements.txt
-pytest==8.3.2
+pytest==8.3.3
 pytest-cov==5.0.0
 flake8==7.1.1
 bump2version==1.0.1

From 304aaa5e460166929431b50c3161a6f6b3152739 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 4 Sep 2024 00:02:33 +0000
Subject: [PATCH 224/275] Bump cryptography from 42.0.3 to 43.0.1

Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.3 to 43.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.3...43.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 63 ++++++++++++++++++++++++-----------------------------
 1 file changed, 29 insertions(+), 34 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index b0b231889..54aabdab0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -265,43 +265,38 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "42.0.3"
+version = "43.0.1"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"},
-    {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"},
-    {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"},
-    {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"},
-    {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"},
-    {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"},
-    {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"},
-    {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"},
-    {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"},
-    {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"},
-    {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"},
-    {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"},
-    {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"},
-    {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"},
-    {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"},
-    {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"},
-    {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"},
-    {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"},
-    {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"},
-    {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"},
-    {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"},
-    {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"},
-    {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"},
-    {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"},
-    {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"},
-    {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"},
-    {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"},
-    {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"},
-    {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"},
-    {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"},
-    {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"},
-    {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"},
+    {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
+    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
+    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
+    {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
+    {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
+    {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
+    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
+    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
+    {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
+    {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
+    {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
+    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
+    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
+    {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
+    {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
+    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
+    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
+    {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
+    {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
 ]
 
 [package.dependencies]
@@ -314,7 +309,7 @@ nox = ["nox"]
 pep8test = ["check-sdist", "click", "mypy", "ruff"]
 sdist = ["build"]
 ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
 test-randomorder = ["pytest-randomly"]
 
 [[package]]

From 2f3171670c6188eb842912bf0ab7f93dc0da179b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 12 Sep 2024 10:24:09 +0000
Subject: [PATCH 225/275] Bump pytest from 8.3.2 to 8.3.3

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 54aabdab0..449a8e0d8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -622,13 +622,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "pytest"
-version = "8.3.2"
+version = "8.3.3"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
-    {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
+    {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
+    {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
 ]
 
 [package.dependencies]

From 6abda35fcee5d02ceaccd60d27d11a20b5072ed0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Oct 2024 10:38:30 +0000
Subject: [PATCH 226/275] Bump pytest-cov from 5.0.0 to 6.0.0

Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock    | 132 ++++++++++++++++++++++++++-----------------------
 pyproject.toml |   2 +-
 2 files changed, 72 insertions(+), 62 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 449a8e0d8..aee7ac638 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -198,63 +198,73 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.4.1"
+version = "7.6.4"
 description = "Code coverage measurement for Python"
 optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"},
-    {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"},
-    {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"},
-    {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"},
-    {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"},
-    {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"},
-    {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"},
-    {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"},
-    {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"},
-    {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"},
-    {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"},
-    {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"},
-    {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"},
-    {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"},
-    {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"},
-    {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"},
-    {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"},
-    {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"},
-    {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"},
-    {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"},
-    {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"},
-    {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"},
+python-versions = ">=3.9"
+files = [
+    {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"},
+    {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"},
+    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"},
+    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"},
+    {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"},
+    {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"},
+    {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"},
+    {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"},
+    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"},
+    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"},
+    {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"},
+    {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"},
+    {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"},
+    {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"},
+    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"},
+    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"},
+    {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"},
+    {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"},
+    {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"},
+    {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"},
+    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"},
+    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"},
+    {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"},
+    {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"},
+    {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"},
+    {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"},
+    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"},
+    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"},
+    {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"},
+    {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"},
+    {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"},
+    {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"},
+    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"},
+    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"},
+    {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"},
+    {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"},
+    {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"},
+    {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"},
 ]
 
 [package.dependencies]
@@ -644,17 +654,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
 
 [[package]]
 name = "pytest-cov"
-version = "5.0.0"
+version = "6.0.0"
 description = "Pytest plugin for measuring coverage."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
-    {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
+    {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
+    {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
 ]
 
 [package.dependencies]
-coverage = {version = ">=5.2.1", extras = ["toml"]}
+coverage = {version = ">=7.5", extras = ["toml"]}
 pytest = ">=4.6"
 
 [package.extras]
@@ -840,4 +850,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "e38482dec55172a5bf913f817cbc144671935895fed8e92e8d7be7d4ba759fd4"
+content-hash = "548a3f3f2a1106f75229f87b8bf98acb0cc4649e7b0905b5908b4de8af848f64"
diff --git a/pyproject.toml b/pyproject.toml
index 10001f865..25c9cde51 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ python = "^3.10"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^8.0.1"
-pytest-cov = ">=4.1,<6.0"
+pytest-cov = ">=4.1,<7.0"
 flake8 = "^7.0.0"
 pyflakes = "^3.2.0"
 bump2version = "^1.0.1"

From 4a1fe0fefe309e26068c35ce0d04df8b610c80d3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Dec 2024 11:38:55 +0000
Subject: [PATCH 227/275] Bump pytest from 8.3.3 to 8.3.4

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-tests.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-tests.txt b/requirements-tests.txt
index 68b83b327..44b2b01a1 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,5 +1,5 @@
 -r requirements.txt
-pytest==8.3.3
+pytest==8.3.4
 pytest-cov==5.0.0
 flake8==7.1.1
 bump2version==1.0.1

From 012fbcb587cc1f849a94619dee3b4a9ac42f4a36 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Dec 2024 11:39:09 +0000
Subject: [PATCH 228/275] Bump twine from 5.1.1 to 6.0.1

Bumps [twine](https://github.com/pypa/twine) from 5.1.1 to 6.0.1.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/v5.1.1...6.0.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock    | 17 ++++++++++-------
 pyproject.toml |  2 +-
 2 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index aee7ac638..1cd3e9586 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -795,19 +795,19 @@ files = [
 
 [[package]]
 name = "twine"
-version = "5.1.1"
+version = "6.0.1"
 description = "Collection of utilities for publishing packages on PyPI"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997"},
-    {file = "twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db"},
+    {file = "twine-6.0.1-py3-none-any.whl", hash = "sha256:9c6025b203b51521d53e200f4a08b116dee7500a38591668c6a6033117bdc218"},
+    {file = "twine-6.0.1.tar.gz", hash = "sha256:36158b09df5406e1c9c1fb8edb24fc2be387709443e7376689b938531582ee27"},
 ]
 
 [package.dependencies]
-importlib-metadata = ">=3.6"
-keyring = ">=15.1"
-pkginfo = ">=1.8.1,<1.11"
+keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""}
+packaging = "*"
+pkginfo = ">=1.8.1"
 readme-renderer = ">=35.0"
 requests = ">=2.20"
 requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
@@ -815,6 +815,9 @@ rfc3986 = ">=1.4.0"
 rich = ">=12.0.0"
 urllib3 = ">=1.26.0"
 
+[package.extras]
+keyring = ["keyring (>=15.1)"]
+
 [[package]]
 name = "urllib3"
 version = "2.2.2"
@@ -850,4 +853,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "548a3f3f2a1106f75229f87b8bf98acb0cc4649e7b0905b5908b4de8af848f64"
+content-hash = "ba115b9d4bdc57e279ffb3101b43bf19b58225d31d5e846715d1616d312d5884"
diff --git a/pyproject.toml b/pyproject.toml
index 25c9cde51..7c93edc94 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,7 @@ pytest-cov = ">=4.1,<7.0"
 flake8 = "^7.0.0"
 pyflakes = "^3.2.0"
 bump2version = "^1.0.1"
-twine = "^5.0.0"
+twine = ">=5,<7"
 
 [build-system]
 requires = ["poetry-core"]

From 7c2b3cd30e05e029a773edc6f10d68287219ddbc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 3 Dec 2024 10:44:41 +0000
Subject: [PATCH 229/275] Bump pytest from 8.3.3 to 8.3.4

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 1cd3e9586..41a499759 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -632,13 +632,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "pytest"
-version = "8.3.3"
+version = "8.3.4"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
-    {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
+    {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
+    {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
 ]
 
 [package.dependencies]

From b346e976eb79980fe7826b7e89be0f16bbc89b38 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 22 Jan 2025 10:35:26 +0000
Subject: [PATCH 230/275] Bump twine from 6.0.1 to 6.1.0

Bumps [twine](https://github.com/pypa/twine) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/6.0.1...6.1.0)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 53 +++++++++++++++++++++++++++++------------------------
 1 file changed, 29 insertions(+), 24 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 41a499759..6c2a30502 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
 [[package]]
 name = "bump2version"
@@ -363,6 +363,25 @@ mccabe = ">=0.7.0,<0.8.0"
 pycodestyle = ">=2.12.0,<2.13.0"
 pyflakes = ">=3.2.0,<3.3.0"
 
+[[package]]
+name = "id"
+version = "1.5.0"
+description = "A tool for generating OIDC identities"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"},
+    {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"},
+]
+
+[package.dependencies]
+requests = "*"
+
+[package.extras]
+dev = ["build", "bump (>=1.3.2)", "id[lint,test]"]
+lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"]
+test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"]
+
 [[package]]
 name = "idna"
 version = "3.7"
@@ -544,29 +563,15 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "23.2"
+version = "24.2"
 description = "Core utilities for Python packages"
 optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
-    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
-]
-
-[[package]]
-name = "pkginfo"
-version = "1.9.6"
-description = "Query metadata from sdists / bdists / installed packages."
-optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"},
-    {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"},
+    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 ]
 
-[package.extras]
-testing = ["pytest", "pytest-cov"]
-
 [[package]]
 name = "pluggy"
 version = "1.5.0"
@@ -795,19 +800,19 @@ files = [
 
 [[package]]
 name = "twine"
-version = "6.0.1"
+version = "6.1.0"
 description = "Collection of utilities for publishing packages on PyPI"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "twine-6.0.1-py3-none-any.whl", hash = "sha256:9c6025b203b51521d53e200f4a08b116dee7500a38591668c6a6033117bdc218"},
-    {file = "twine-6.0.1.tar.gz", hash = "sha256:36158b09df5406e1c9c1fb8edb24fc2be387709443e7376689b938531582ee27"},
+    {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"},
+    {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"},
 ]
 
 [package.dependencies]
+id = "*"
 keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""}
-packaging = "*"
-pkginfo = ">=1.8.1"
+packaging = ">=24.0"
 readme-renderer = ">=35.0"
 requests = ">=2.20"
 requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"

From cda60a52331c7102cff892b9b77c8321e276680a Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 08:49:01 +1100
Subject: [PATCH 231/275] fix: update nix flake to fix problems

---
 flake.lock | 43 ++++++++++++++++++++++---------------------
 flake.nix  | 44 +++++++++++++++++++++++++++++---------------
 2 files changed, 51 insertions(+), 36 deletions(-)

diff --git a/flake.lock b/flake.lock
index ac93bf25c..1748fe433 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1705309234,
-        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
         "type": "github"
       },
       "original": {
@@ -23,11 +23,11 @@
         "systems": "systems_2"
       },
       "locked": {
-        "lastModified": 1694529238,
-        "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+        "lastModified": 1726560853,
+        "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+        "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
         "type": "github"
       },
       "original": {
@@ -44,11 +44,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1698974481,
-        "narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
+        "lastModified": 1729742964,
+        "narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
         "owner": "nix-community",
         "repo": "nix-github-actions",
-        "rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
+        "rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
         "type": "github"
       },
       "original": {
@@ -59,16 +59,16 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708161998,
-        "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=",
+        "lastModified": 1738702386,
+        "narHash": "sha256-nJj8f78AYAxl/zqLiFGXn5Im1qjFKU8yBPKoWEeZN5M=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "84d981bae8b5e783b3b548de505b22880559515f",
+        "rev": "030ba1976b7c0e1a67d9716b17308ccdab5b381e",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "nixos-23.11",
+        "ref": "nixos-24.11",
         "repo": "nixpkgs",
         "type": "github"
       }
@@ -84,11 +84,11 @@
         "treefmt-nix": "treefmt-nix"
       },
       "locked": {
-        "lastModified": 1708175019,
-        "narHash": "sha256-B7wY2pNrLc3X9uYRo1LUmVzI6oH6fX8oi+96GdUpayU=",
+        "lastModified": 1738741221,
+        "narHash": "sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE=",
         "owner": "nix-community",
         "repo": "poetry2nix",
-        "rev": "403d923ea8e2e6cedce3a0f04a9394c4244cb806",
+        "rev": "be1fe795035d3d36359ca9135b26dcc5321b31fb",
         "type": "github"
       },
       "original": {
@@ -144,8 +144,9 @@
         "type": "github"
       },
       "original": {
-        "id": "systems",
-        "type": "indirect"
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
       }
     },
     "treefmt-nix": {
@@ -156,11 +157,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1699786194,
-        "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
+        "lastModified": 1730120726,
+        "narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
+        "rev": "9ef337e492a5555d8e17a51c911ff1f02635be15",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 78abd53db..f044ec532 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,24 +1,32 @@
 {
-  description =
-    "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling.";
+  description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling.";
 
   inputs.flake-utils.url = "github:numtide/flake-utils";
-  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
+  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
   inputs.poetry2nix = {
     url = "github:nix-community/poetry2nix";
     inputs.nixpkgs.follows = "nixpkgs";
   };
 
-  outputs = { self, nixpkgs, flake-utils, poetry2nix }:
-    flake-utils.lib.eachDefaultSystem (system:
+  outputs =
+    {
+      self,
+      nixpkgs,
+      flake-utils,
+      poetry2nix,
+    }:
+    flake-utils.lib.eachDefaultSystem (
+      system:
       let
         p2n = import poetry2nix { inherit pkgs; };
-        overrides = p2n.defaultPoetryOverrides.extend (self: super: {
-          nh3 = super.nh3.override { preferWheel = true; };
-          bump2version = super.bump2version.overridePythonAttrs (old: {
-            buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ];
-          });
-        });
+        overrides = p2n.defaultPoetryOverrides.extend (
+          self: super: {
+            nh3 = super.nh3.override { preferWheel = true; };
+            bump2version = super.bump2version.overridePythonAttrs (old: {
+              buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ];
+            });
+          }
+        );
 
         poetry_env = p2n.mkPoetryEnv {
           python = pkgs.python3;
@@ -31,12 +39,18 @@
           inherit overrides;
         };
         pkgs = nixpkgs.legacyPackages.${system};
-      in {
+      in
+      {
         packages = {
           sshuttle = poetry_app;
           default = self.packages.${system}.sshuttle;
         };
-        devShells.default =
-          pkgs.mkShell { packages = [ pkgs.poetry poetry_env ]; };
-      });
+        devShells.default = pkgs.mkShell {
+          packages = [
+            pkgs.poetry
+            poetry_env
+          ];
+        };
+      }
+    );
 }

From 1084c0f2458c1595b00963b3bd54bd667e4cfc9f Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 08:54:17 +1100
Subject: [PATCH 232/275] fix: drop Python 3.8 support

Python 3.8 support has been dropped upstream.
---
 .github/workflows/pythonpackage.yml | 2 +-
 docs/requirements.rst               | 4 ++--
 scripts/README.md                   | 2 +-
 setup.py                            | 3 +--
 tox.ini                             | 1 -
 5 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 166d9d8a7..1d18f0e77 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10"]
+        python-version: ["3.9", "3.10"]
 
     steps:
     - uses: actions/checkout@v4
diff --git a/docs/requirements.rst b/docs/requirements.rst
index f5a0936fb..3d4d3ec95 100644
--- a/docs/requirements.rst
+++ b/docs/requirements.rst
@@ -6,7 +6,7 @@ Client side Requirements
 
 - sudo, or root access on your client machine.
   (The server doesn't need admin access.)
-- Python 3.8 or greater.
+- Python 3.9 or greater.
 
 
 Linux with NAT method
@@ -71,7 +71,7 @@ Experimental built-in support available. See :doc:`windows` for more information
 Server side Requirements
 ------------------------
 
-- Python 3.8 or greater.
+- Python 3.9 or greater.
 
 
 Additional Suggested Software
diff --git a/scripts/README.md b/scripts/README.md
index 878d4f956..e9204c456 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -3,7 +3,7 @@
 ```bash
 test-bed up -d # start containers
 
-exec-sshuttle  [--copy-id] [--server-py=2.7|3.6|3.8] [--client-py=2.7|3.6|3.8] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...]
+exec-sshuttle  [--copy-id] [--server-py=2.7|3.10] [--client-py=2.7|3.10] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...]
     # --copy-id  -> optionally do ssh-copy-id to make it passwordless for future runs
     # --sshuttle-bin -> use another sshuttle binary instead of one from dev setup
     # --server-py  -> Python version to use in server. (manged by pyenv)
diff --git a/setup.py b/setup.py
index d51c5f6ed..9010755b9 100755
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,6 @@
         "License :: OSI Approved :: " +
             "GNU Lesser General Public License v2 or later (LGPLv2+)",
         "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Topic :: System :: Networking",
@@ -48,7 +47,7 @@
             'sshuttle = sshuttle.cmdline:main',
         ],
     },
-    python_requires='>=3.8',
+    python_requires='>=3.9',
     install_requires=[
         "pydivert; os_name=='nt'"
     ],
diff --git a/tox.ini b/tox.ini
index b63d87c93..b2ead0f24 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,6 @@ envlist =
 
 [testenv]
 basepython =
-    py38: python3.8
     py39: python3.9
     py310: python3.10
 commands =

From 339b5221bc33254329f79f2374f6114be6f30aed Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 09:06:29 +1100
Subject: [PATCH 233/275] fix: Remove more references to legacy Python versions

---
 scripts/Containerfile | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/scripts/Containerfile b/scripts/Containerfile
index 9bf96e857..ff01e6ffa 100644
--- a/scripts/Containerfile
+++ b/scripts/Containerfile
@@ -7,9 +7,6 @@ FROM ${BASE_IMAGE} as pyenv
 RUN apk add --no-cache build-base git libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev
 ENV PYENV_ROOT=/pyenv
 RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
-RUN /pyenv/bin/pyenv install 2.7
-RUN /pyenv/bin/pyenv install 3.6
-RUN /pyenv/bin/pyenv install 3.8
 RUN /pyenv/bin/pyenv install 3.10
 RUN bash -xc 'rm -rf /pyenv/{.git,plugins} /pyenv/versions/*/lib/*/{test,config,config-*linux-gnu}' && \
     find /pyenv -type d -name __pycache__ -exec rm -rf {} + && \

From a3396a443df14d3bafc3d25909d9221aa182b8fc Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 09:06:29 +1100
Subject: [PATCH 234/275] fix: Add support for Python 3.11 and Python 3.11

---
 .github/workflows/pythonpackage.yml | 2 +-
 scripts/Containerfile               | 4 +++-
 setup.py                            | 2 ++
 tox.ini                             | 2 ++
 4 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 1d18f0e77..b19d6eee0 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.9", "3.10"]
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
 
     steps:
     - uses: actions/checkout@v4
diff --git a/scripts/Containerfile b/scripts/Containerfile
index ff01e6ffa..3f6df8a68 100644
--- a/scripts/Containerfile
+++ b/scripts/Containerfile
@@ -8,6 +8,8 @@ RUN apk add --no-cache build-base git libffi-dev openssl-dev bzip2-dev zlib-dev
 ENV PYENV_ROOT=/pyenv
 RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
 RUN /pyenv/bin/pyenv install 3.10
+RUN /pyenv/bin/pyenv install 3.11
+RUN /pyenv/bin/pyenv install 3.12
 RUN bash -xc 'rm -rf /pyenv/{.git,plugins} /pyenv/versions/*/lib/*/{test,config,config-*linux-gnu}' && \
     find /pyenv -type d -name __pycache__ -exec rm -rf {} + && \
     find /pyenv -type f -name '*.py[co]' -delete
@@ -34,4 +36,4 @@ RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run
 
 # https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers
 # To customize the container and start other components 
-COPY container.setup.sh /custom-cont-init.d/setup.sh
\ No newline at end of file
+COPY container.setup.sh /custom-cont-init.d/setup.sh
diff --git a/setup.py b/setup.py
index 9010755b9..98af52d6b 100755
--- a/setup.py
+++ b/setup.py
@@ -40,6 +40,8 @@
         "Operating System :: OS Independent",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Topic :: System :: Networking",
     ],
     entry_points={
diff --git a/tox.ini b/tox.ini
index b2ead0f24..1c95a3546 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,6 +9,8 @@ envlist =
 basepython =
     py39: python3.9
     py310: python3.10
+    py311: python3.11
+    py312: python3.12
 commands =
     pip install -e .
     # actual flake8 test

From 4b6f7c6a656a752552295863092d3b8af0b42b31 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 09:11:01 +1100
Subject: [PATCH 235/275] fix: fix broken workflow_dispatch CI rule

---
 .github/workflows/pythonpackage.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index b19d6eee0..6913ae553 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -8,8 +8,7 @@ on:
     branches: [ master ]
   pull_request:
     branches: [ master ]
-  workflow_dispatch:
-    branches: [ master ]
+  workflow_dispatch: {}
 
 jobs:
   build:

From 6f126982090d94a11551c117f243666e96500f44 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:14:51 +0000
Subject: [PATCH 236/275] Bump pytest-cov from 5.0.0 to 6.0.0

Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 requirements-tests.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-tests.txt b/requirements-tests.txt
index 44b2b01a1..86d97cfc5 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,5 +1,5 @@
 -r requirements.txt
 pytest==8.3.4
-pytest-cov==5.0.0
+pytest-cov==6.0.0
 flake8==7.1.1
 bump2version==1.0.1

From 85dc3199a332f9f9f0e4c6037c883a8f88dc09ca Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 15:51:02 +1100
Subject: [PATCH 237/275] fix: replace requirements.txt files with poetry

---
 .github/workflows/pythonpackage.yml |   26 +-
 .readthedocs.yaml                   |    6 +
 poetry.lock                         | 1135 +++++++++++++++++++--------
 pyproject.toml                      |    7 +
 requirements-tests.txt              |    5 -
 requirements.txt                    |    2 -
 6 files changed, 832 insertions(+), 349 deletions(-)
 delete mode 100644 requirements-tests.txt
 delete mode 100644 requirements.txt

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 6913ae553..1eeacdef3 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -17,20 +17,30 @@ jobs:
     strategy:
       matrix:
         python-version: ["3.9", "3.10", "3.11", "3.12"]
-
+        poetry-version: ["main"]
     steps:
     - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
+    - name: Run image
+      uses: abatilo/actions-poetry@v3
+      with:
+        poetry-version: ${{ matrix.poetry-version }}
+    - name: Setup a local virtual environment (if no poetry.toml file)
       run: |
-        python -m pip install --upgrade pip
-        pip install -r requirements-tests.txt
+        poetry config virtualenvs.create true --local
+        poetry config virtualenvs.in-project true --local
+    - uses: actions/cache@v3
+      name: Define a cache for the virtual environment based on the dependencies lock file
+      with:
+        path: ./.venv
+        key: venv-${{ hashFiles('poetry.lock') }}
+    - name: Install the project dependencies
+      run: poetry install
     - name: Lint with flake8
       run: |
-        flake8 sshuttle tests --count --show-source --statistics
-    - name: Test with pytest
-      run: |
-        PYTHONPATH=$PWD pytest
+        poetry run flake8 sshuttle tests --count --show-source --statistics
+    - name: Run the automated tests
+      run: poetry run pytest -v
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 217c7df19..a8b03127b 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -4,6 +4,12 @@ build:
   os: ubuntu-20.04
   tools:
     python: "3.9"
+  jobs:
+    post_create_environment:
+      - pip install poetry
+      - poetry config virtualenvs.create false
+    post_install:
+      - poetry install --with docs
 
 sphinx:
   configuration: docs/conf.py
diff --git a/poetry.lock b/poetry.lock
index 6c2a30502..5f7ef2d6d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,66 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+
+[[package]]
+name = "alabaster"
+version = "0.7.16"
+description = "A light, configurable Sphinx theme"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"},
+    {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
+]
+
+[[package]]
+name = "babel"
+version = "2.17.0"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"},
+    {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"},
+]
+
+[package.extras]
+dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"]
+
+[[package]]
+name = "backports-tarfile"
+version = "1.2.0"
+description = "Backport of CPython tarfile module"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"},
+    {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.3"
+description = "Screen-scraping library"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"},
+    {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+typing-extensions = ">=4.0.0"
+
+[package.extras]
+cchardet = ["cchardet"]
+chardet = ["chardet"]
+charset-normalizer = ["charset-normalizer"]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
 
 [[package]]
 name = "bump2version"
@@ -13,74 +75,89 @@ files = [
 
 [[package]]
 name = "certifi"
-version = "2024.7.4"
+version = "2025.1.31"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
-    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+    {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
+    {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
 ]
 
 [[package]]
 name = "cffi"
-version = "1.16.0"
+version = "1.17.1"
 description = "Foreign Function Interface for Python calling C code."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
-    {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
-    {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
-    {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
-    {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
-    {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
-    {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
-    {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
-    {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
-    {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
-    {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
-    {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
-    {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
-    {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
-    {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
-    {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
-    {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
-    {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
-    {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
-    {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
-    {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
-    {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
-    {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
-    {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
-    {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
-    {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
-    {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
-    {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
-    {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
-    {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
-    {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
-    {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
+    {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+    {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+    {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+    {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+    {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+    {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+    {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+    {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+    {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+    {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
 ]
 
 [package.dependencies]
@@ -88,101 +165,103 @@ pycparser = "*"
 
 [[package]]
 name = "charset-normalizer"
-version = "3.3.2"
+version = "3.4.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 optional = false
-python-versions = ">=3.7.0"
+python-versions = ">=3.7"
 files = [
-    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
-    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
+    {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
+    {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
+    {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
+    {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
+    {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
+    {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
 ]
 
 [[package]]
@@ -198,73 +277,73 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.6.4"
+version = "7.6.10"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"},
-    {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"},
-    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"},
-    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"},
-    {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"},
-    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"},
-    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"},
-    {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"},
-    {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"},
-    {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"},
-    {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"},
-    {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"},
-    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"},
-    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"},
-    {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"},
-    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"},
-    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"},
-    {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"},
-    {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"},
-    {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"},
-    {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"},
-    {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"},
-    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"},
-    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"},
-    {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"},
-    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"},
-    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"},
-    {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"},
-    {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"},
-    {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"},
-    {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"},
-    {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"},
-    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"},
-    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"},
-    {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"},
-    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"},
-    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"},
-    {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"},
-    {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"},
-    {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"},
-    {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"},
-    {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"},
-    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"},
-    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"},
-    {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"},
-    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"},
-    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"},
-    {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"},
-    {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"},
-    {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"},
-    {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"},
-    {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"},
-    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"},
-    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"},
-    {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"},
-    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"},
-    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"},
-    {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"},
-    {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"},
-    {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"},
-    {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"},
-    {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"},
+    {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
+    {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
+    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
+    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
+    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
+    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
+    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
+    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
+    {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
+    {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
+    {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
+    {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
+    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
+    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
+    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
+    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
+    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
+    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
+    {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
+    {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
+    {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
+    {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
+    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
+    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
+    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
+    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
+    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
+    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
+    {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
+    {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
+    {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
+    {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
+    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
+    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
+    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
+    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
+    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
+    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
+    {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
+    {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
+    {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
+    {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
+    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
+    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
+    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
+    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
+    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
+    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
+    {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
+    {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
+    {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
+    {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
+    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
+    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
+    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
+    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
+    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
+    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
+    {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
+    {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
+    {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
+    {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
 ]
 
 [package.dependencies]
@@ -275,51 +354,51 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "43.0.1"
+version = "44.0.0"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 optional = false
-python-versions = ">=3.7"
+python-versions = "!=3.9.0,!=3.9.1,>=3.7"
 files = [
-    {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
-    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
-    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
-    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
-    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
-    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
-    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
-    {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
-    {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
-    {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
-    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
-    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
-    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
-    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
-    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
-    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
-    {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
-    {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
-    {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
-    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
-    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
-    {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
-    {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
-    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
-    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
-    {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
-    {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
+    {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
+    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
+    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
+    {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
+    {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"},
+    {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
+    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
+    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
+    {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
+    {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"},
+    {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"},
+    {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"},
 ]
 
 [package.dependencies]
 cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
 
 [package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
-docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
-nox = ["nox"]
-pep8test = ["check-sdist", "click", "mypy", "ruff"]
-sdist = ["build"]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
+docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
+nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+sdist = ["build (>=1.0.0)"]
 ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
 test-randomorder = ["pytest-randomly"]
 
 [[package]]
@@ -335,13 +414,13 @@ files = [
 
 [[package]]
 name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.2"
 description = "Backport of PEP 654 (exception groups)"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
-    {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+    {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+    {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
 ]
 
 [package.extras]
@@ -363,6 +442,23 @@ mccabe = ">=0.7.0,<0.8.0"
 pycodestyle = ">=2.12.0,<2.13.0"
 pyflakes = ">=3.2.0,<3.3.0"
 
+[[package]]
+name = "furo"
+version = "2024.8.6"
+description = "A clean customisable Sphinx documentation theme."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"},
+    {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+pygments = ">=2.7"
+sphinx = ">=6.0,<9.0"
+sphinx-basic-ng = ">=1.0.0.beta2"
+
 [[package]]
 name = "id"
 version = "1.5.0"
@@ -384,33 +480,51 @@ test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"]
 
 [[package]]
 name = "idna"
-version = "3.7"
+version = "3.10"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 files = [
-    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
-    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+    {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+    {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
+    {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
 ]
 
 [[package]]
 name = "importlib-metadata"
-version = "7.0.1"
+version = "8.6.1"
 description = "Read metadata from Python packages"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"},
-    {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"},
+    {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
+    {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
 ]
 
 [package.dependencies]
-zipp = ">=0.5"
+zipp = ">=3.20"
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
 perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
+type = ["pytest-mypy"]
 
 [[package]]
 name = "iniconfig"
@@ -425,22 +539,62 @@ files = [
 
 [[package]]
 name = "jaraco-classes"
-version = "3.3.1"
+version = "3.4.0"
 description = "Utility functions for Python class constructs"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"},
-    {file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"},
+    {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"},
+    {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"},
 ]
 
 [package.dependencies]
 more-itertools = "*"
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
 
+[[package]]
+name = "jaraco-context"
+version = "6.0.1"
+description = "Useful decorators and context managers"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"},
+    {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"},
+]
+
+[package.dependencies]
+"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""}
+
+[package.extras]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+
+[[package]]
+name = "jaraco-functools"
+version = "4.1.0"
+description = "Functools like those found in stdlib"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"},
+    {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"},
+]
+
+[package.dependencies]
+more-itertools = "*"
+
+[package.extras]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"]
+type = ["pytest-mypy"]
+
 [[package]]
 name = "jeepney"
 version = "0.8.0"
@@ -456,28 +610,51 @@ files = [
 test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
 trio = ["async_generator", "trio"]
 
+[[package]]
+name = "jinja2"
+version = "3.1.5"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
+    {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
 [[package]]
 name = "keyring"
-version = "24.3.0"
+version = "25.6.0"
 description = "Store and access your passwords safely."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"},
-    {file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"},
+    {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"},
+    {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"},
 ]
 
 [package.dependencies]
-importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
+importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
 "jaraco.classes" = "*"
+"jaraco.context" = "*"
+"jaraco.functools" = "*"
 jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
 pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
 SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
 
 [package.extras]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
 completion = ["shtab (>=1.1.0)"]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["pyfakefs", "pytest (>=6,!=8.1.*)"]
+type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"]
 
 [[package]]
 name = "markdown-it-py"
@@ -503,6 +680,76 @@ profiling = ["gprof2dot"]
 rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
 testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
+    {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
+    {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
+    {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
+]
+
 [[package]]
 name = "mccabe"
 version = "0.7.0"
@@ -527,38 +774,46 @@ files = [
 
 [[package]]
 name = "more-itertools"
-version = "10.2.0"
+version = "10.6.0"
 description = "More routines for operating on iterables, beyond itertools"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"},
-    {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"},
+    {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"},
+    {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"},
 ]
 
 [[package]]
 name = "nh3"
-version = "0.2.15"
-description = "Python bindings to the ammonia HTML sanitization library."
+version = "0.2.20"
+description = "Python binding to Ammonia HTML sanitizer Rust crate"
 optional = false
-python-versions = "*"
+python-versions = ">=3.8"
 files = [
-    {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"},
-    {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"},
-    {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"},
-    {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"},
-    {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"},
-    {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"},
-    {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"},
-    {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"},
-    {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"},
-    {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"},
+    {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"},
+    {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"},
+    {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"},
+    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"},
+    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"},
+    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"},
+    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"},
+    {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"},
+    {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"},
+    {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"},
+    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"},
+    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"},
+    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"},
+    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"},
+    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"},
+    {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"},
+    {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"},
+    {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"},
 ]
 
 [[package]]
@@ -589,24 +844,24 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pycodestyle"
-version = "2.12.0"
+version = "2.12.1"
 description = "Python style guide checker"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"},
-    {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"},
+    {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
+    {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
 ]
 
 [[package]]
 name = "pycparser"
-version = "2.21"
+version = "2.22"
 description = "C parser in Python"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.8"
 files = [
-    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
-    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+    {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+    {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
 ]
 
 [[package]]
@@ -622,17 +877,16 @@ files = [
 
 [[package]]
 name = "pygments"
-version = "2.17.2"
+version = "2.19.1"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
-    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+    {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
+    {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
 ]
 
 [package.extras]
-plugins = ["importlib-metadata"]
 windows-terminal = ["colorama (>=0.4.6)"]
 
 [[package]]
@@ -677,24 +931,24 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
 
 [[package]]
 name = "pywin32-ctypes"
-version = "0.2.2"
+version = "0.2.3"
 description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
-    {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
+    {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
+    {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
 ]
 
 [[package]]
 name = "readme-renderer"
-version = "42.0"
+version = "43.0"
 description = "readme_renderer is a library for rendering readme descriptions for Warehouse"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "readme_renderer-42.0-py3-none-any.whl", hash = "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d"},
-    {file = "readme_renderer-42.0.tar.gz", hash = "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1"},
+    {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"},
+    {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"},
 ]
 
 [package.dependencies]
@@ -707,13 +961,13 @@ md = ["cmarkgfm (>=0.8.0)"]
 
 [[package]]
 name = "requests"
-version = "2.32.0"
+version = "2.32.3"
 description = "Python HTTP for Humans."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"},
-    {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"},
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
 ]
 
 [package.dependencies]
@@ -756,18 +1010,19 @@ idna2008 = ["idna"]
 
 [[package]]
 name = "rich"
-version = "13.7.0"
+version = "13.9.4"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 optional = false
-python-versions = ">=3.7.0"
+python-versions = ">=3.8.0"
 files = [
-    {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
-    {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
+    {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
+    {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
 ]
 
 [package.dependencies]
 markdown-it-py = ">=2.2.0"
 pygments = ">=2.13.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
 jupyter = ["ipywidgets (>=7.5.1,<9)"]
@@ -787,15 +1042,212 @@ files = [
 cryptography = ">=2.0"
 jeepney = ">=0.6"
 
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+optional = false
+python-versions = "*"
+files = [
+    {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+    {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.6"
+description = "A modern CSS selector implementation for Beautiful Soup."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
+    {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
+]
+
+[[package]]
+name = "sphinx"
+version = "7.1.2"
+description = "Python documentation generator"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
+    {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
+]
+
+[package.dependencies]
+alabaster = ">=0.7,<0.8"
+babel = ">=2.9"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.18.1,<0.21"
+imagesize = ">=1.3"
+Jinja2 = ">=3.0"
+packaging = ">=21.0"
+Pygments = ">=2.13"
+requests = ">=2.25.0"
+snowballstemmer = ">=2.0"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
+test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
+
+[[package]]
+name = "sphinx-basic-ng"
+version = "1.0.0b2"
+description = "A modern skeleton for Sphinx themes."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"},
+    {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"},
+]
+
+[package.dependencies]
+sphinx = ">=4.0"
+
+[package.extras]
+docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "2.0.0"
+description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"},
+    {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"},
+]
+
+[package.extras]
+lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "2.0.0"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"},
+    {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"},
+]
+
+[package.extras]
+lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.1.0"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"},
+    {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"},
+]
+
+[package.extras]
+lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
+standalone = ["Sphinx (>=5)"]
+test = ["html5lib", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+    {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+
+[package.extras]
+test = ["flake8", "mypy", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "2.0.0"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"},
+    {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"},
+]
+
+[package.extras]
+lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
+standalone = ["Sphinx (>=5)"]
+test = ["defusedxml (>=0.7.1)", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "2.0.0"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"},
+    {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"},
+]
+
+[package.extras]
+lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
 [[package]]
 name = "tomli"
-version = "2.0.1"
+version = "2.2.1"
 description = "A lil' TOML parser"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
-    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+    {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+    {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+    {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+    {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+    {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+    {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+    {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+    {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+    {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+    {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+    {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+    {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+    {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+    {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
 ]
 
 [[package]]
@@ -823,15 +1275,26 @@ urllib3 = ">=1.26.0"
 [package.extras]
 keyring = ["keyring (>=15.1)"]
 
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
+
 [[package]]
 name = "urllib3"
-version = "2.2.2"
+version = "2.3.0"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
-    {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+    {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
+    {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
 ]
 
 [package.extras]
@@ -842,20 +1305,24 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "zipp"
-version = "3.19.1"
+version = "3.21.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"},
-    {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"},
+    {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
+    {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
 ]
 
 [package.extras]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
 doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
+type = ["pytest-mypy"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "ba115b9d4bdc57e279ffb3101b43bf19b58225d31d5e846715d1616d312d5884"
+content-hash = "01fd67f2e659d25ed4a4a6e76ed50c38ccddd0c442354d72a70e6c8987682478"
diff --git a/pyproject.toml b/pyproject.toml
index 7c93edc94..7d3ac2f97 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,3 +23,10 @@ build-backend = "poetry.core.masonry.api"
 
 [tool.poetry.scripts]
 sshuttle = "sshuttle.cmdline:main"
+
+[tool.poetry.group.docs]
+optional = true
+
+[tool.poetry.group.docs.dependencies]
+sphinx = "7.1.2"
+furo = "2024.8.6"
diff --git a/requirements-tests.txt b/requirements-tests.txt
deleted file mode 100644
index 86d97cfc5..000000000
--- a/requirements-tests.txt
+++ /dev/null
@@ -1,5 +0,0 @@
--r requirements.txt
-pytest==8.3.4
-pytest-cov==6.0.0
-flake8==7.1.1
-bump2version==1.0.1
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index fa703fb83..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Sphinx==7.1.2
-furo==2024.8.6

From d08f78a2d9777951d7e18f6eaebbcdd279d7683a Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 16:03:48 +1100
Subject: [PATCH 238/275] fix: replace requirements.txt files with poetry (2)

---
 .readthedocs.yaml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index a8b03127b..8ce218563 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -13,9 +13,3 @@ build:
 
 sphinx:
   configuration: docs/conf.py
-
-python:
-  install:
-    - requirements: requirements.txt
-    - method: setuptools
-      path: .

From 62da70510e8a1f93e8b38870fdebdbace965cd8e Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 16:05:38 +1100
Subject: [PATCH 239/275] fix: replace requirements.txt files with poetry (3)

---
 .readthedocs.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 8ce218563..218eee36f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,7 +3,7 @@ version: 2
 build:
   os: ubuntu-20.04
   tools:
-    python: "3.9"
+    python: "3.10"
   jobs:
     post_create_environment:
       - pip install poetry

From 9bcedf19049e5b3a8ae26818299cc518ec03a926 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 16:47:52 +1100
Subject: [PATCH 240/275] fix: replace requirements.txt files with poetry (4)

---
 .readthedocs.yaml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 218eee36f..0a16c54bc 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -7,9 +7,8 @@ build:
   jobs:
     post_create_environment:
       - pip install poetry
-      - poetry config virtualenvs.create false
     post_install:
-      - poetry install --with docs
+      - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs
 
 sphinx:
   configuration: docs/conf.py

From ac4313decaebd990e535a417d008566213e4516f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 6 Feb 2025 05:52:49 +0000
Subject: [PATCH 241/275] Bump sphinx from 7.1.2 to 8.1.3

Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.2 to 8.1.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.2...v8.1.3)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock    | 47 ++++++++++++++++++++++++-----------------------
 pyproject.toml |  2 +-
 2 files changed, 25 insertions(+), 24 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 5f7ef2d6d..20c737a83 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -1066,37 +1066,38 @@ files = [
 
 [[package]]
 name = "sphinx"
-version = "7.1.2"
+version = "8.1.3"
 description = "Python documentation generator"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
 files = [
-    {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
-    {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
+    {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
+    {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
 ]
 
 [package.dependencies]
-alabaster = ">=0.7,<0.8"
-babel = ">=2.9"
-colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
-docutils = ">=0.18.1,<0.21"
+alabaster = ">=0.7.14"
+babel = ">=2.13"
+colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
+docutils = ">=0.20,<0.22"
 imagesize = ">=1.3"
-Jinja2 = ">=3.0"
-packaging = ">=21.0"
-Pygments = ">=2.13"
-requests = ">=2.25.0"
-snowballstemmer = ">=2.0"
-sphinxcontrib-applehelp = "*"
-sphinxcontrib-devhelp = "*"
-sphinxcontrib-htmlhelp = ">=2.0.0"
-sphinxcontrib-jsmath = "*"
-sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = ">=1.1.5"
+Jinja2 = ">=3.1"
+packaging = ">=23.0"
+Pygments = ">=2.17"
+requests = ">=2.30.0"
+snowballstemmer = ">=2.2"
+sphinxcontrib-applehelp = ">=1.0.7"
+sphinxcontrib-devhelp = ">=1.0.6"
+sphinxcontrib-htmlhelp = ">=2.0.6"
+sphinxcontrib-jsmath = ">=1.0.1"
+sphinxcontrib-qthelp = ">=1.0.6"
+sphinxcontrib-serializinghtml = ">=1.1.9"
+tomli = {version = ">=2", markers = "python_version < \"3.11\""}
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
-test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
+lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
+test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
 
 [[package]]
 name = "sphinx-basic-ng"
@@ -1325,4 +1326,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "01fd67f2e659d25ed4a4a6e76ed50c38ccddd0c442354d72a70e6c8987682478"
+content-hash = "c4803f060fb3180e37102b1380f1043e5015ec2ce5828aed6b7958c81d3d44c6"
diff --git a/pyproject.toml b/pyproject.toml
index 7d3ac2f97..deea59564 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,5 +28,5 @@ sshuttle = "sshuttle.cmdline:main"
 optional = true
 
 [tool.poetry.group.docs.dependencies]
-sphinx = "7.1.2"
+sphinx = "8.1.3"
 furo = "2024.8.6"

From a0d94367f6be85c6c31452bf80697e2a4567e6c3 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 18:55:03 +1100
Subject: [PATCH 242/275] Back out "Bump sphinx from 7.1.2 to 8.1.3"

This backs out commit ac4313decaebd990e535a417d008566213e4516f.
---
 poetry.lock    | 47 +++++++++++++++++++++++------------------------
 pyproject.toml |  2 +-
 2 files changed, 24 insertions(+), 25 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 20c737a83..5f7ef2d6d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -1066,38 +1066,37 @@ files = [
 
 [[package]]
 name = "sphinx"
-version = "8.1.3"
+version = "7.1.2"
 description = "Python documentation generator"
 optional = false
-python-versions = ">=3.10"
+python-versions = ">=3.8"
 files = [
-    {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
-    {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
+    {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
+    {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
 ]
 
 [package.dependencies]
-alabaster = ">=0.7.14"
-babel = ">=2.13"
-colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
-docutils = ">=0.20,<0.22"
+alabaster = ">=0.7,<0.8"
+babel = ">=2.9"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.18.1,<0.21"
 imagesize = ">=1.3"
-Jinja2 = ">=3.1"
-packaging = ">=23.0"
-Pygments = ">=2.17"
-requests = ">=2.30.0"
-snowballstemmer = ">=2.2"
-sphinxcontrib-applehelp = ">=1.0.7"
-sphinxcontrib-devhelp = ">=1.0.6"
-sphinxcontrib-htmlhelp = ">=2.0.6"
-sphinxcontrib-jsmath = ">=1.0.1"
-sphinxcontrib-qthelp = ">=1.0.6"
-sphinxcontrib-serializinghtml = ">=1.1.9"
-tomli = {version = ">=2", markers = "python_version < \"3.11\""}
+Jinja2 = ">=3.0"
+packaging = ">=21.0"
+Pygments = ">=2.13"
+requests = ">=2.25.0"
+snowballstemmer = ">=2.0"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
-test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
+lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
+test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
 
 [[package]]
 name = "sphinx-basic-ng"
@@ -1326,4 +1325,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "c4803f060fb3180e37102b1380f1043e5015ec2ce5828aed6b7958c81d3d44c6"
+content-hash = "01fd67f2e659d25ed4a4a6e76ed50c38ccddd0c442354d72a70e6c8987682478"
diff --git a/pyproject.toml b/pyproject.toml
index deea59564..7d3ac2f97 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,5 +28,5 @@ sshuttle = "sshuttle.cmdline:main"
 optional = true
 
 [tool.poetry.group.docs.dependencies]
-sphinx = "8.1.3"
+sphinx = "7.1.2"
 furo = "2024.8.6"

From 693ee40c485c70f353326eb0e8f721f984850f5c Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 18:48:57 +1100
Subject: [PATCH 243/275] fix: ensure poetry works for Python 3.9

---
 poetry.lock    | 76 ++++++++++++++++++++++++++------------------------
 pyproject.toml |  2 +-
 2 files changed, 40 insertions(+), 38 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 5f7ef2d6d..adcf5c59d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -354,51 +354,51 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "44.0.0"
+version = "43.0.3"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+python-versions = ">=3.7"
 files = [
-    {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
-    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
-    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
-    {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
-    {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"},
-    {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
-    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
-    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
-    {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
-    {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"},
-    {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"},
-    {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"},
+    {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
+    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
+    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
+    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
+    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
+    {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
+    {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
+    {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
+    {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
+    {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
+    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
+    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
+    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
+    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
+    {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
+    {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
+    {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
+    {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
+    {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
+    {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
+    {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
+    {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
+    {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
+    {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
+    {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
+    {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
+    {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
 ]
 
 [package.dependencies]
 cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
 
 [package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
-docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
-pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
-sdist = ["build (>=1.0.0)"]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
+nox = ["nox"]
+pep8test = ["check-sdist", "click", "mypy", "ruff"]
+sdist = ["build"]
 ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
 test-randomorder = ["pytest-randomly"]
 
 [[package]]
@@ -1081,6 +1081,7 @@ babel = ">=2.9"
 colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
 docutils = ">=0.18.1,<0.21"
 imagesize = ">=1.3"
+importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
 Jinja2 = ">=3.0"
 packaging = ">=21.0"
 Pygments = ">=2.13"
@@ -1263,6 +1264,7 @@ files = [
 
 [package.dependencies]
 id = "*"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
 keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""}
 packaging = ">=24.0"
 readme-renderer = ">=35.0"
@@ -1324,5 +1326,5 @@ type = ["pytest-mypy"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.10"
-content-hash = "01fd67f2e659d25ed4a4a6e76ed50c38ccddd0c442354d72a70e6c8987682478"
+python-versions = "^3.9"
+content-hash = "4daba05b7366ab3731b6254c80109031f8b027f3d10127a41bc739f0cb776755"
diff --git a/pyproject.toml b/pyproject.toml
index 7d3ac2f97..f9b5f0ee7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ license = "LGPL-2.1"
 readme = "README.rst"
 
 [tool.poetry.dependencies]
-python = "^3.10"
+python = "^3.9"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^8.0.1"

From bf294643e283cef9fb285d44e307e958686caf46 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Thu, 6 Feb 2025 19:07:15 +1100
Subject: [PATCH 244/275] fix: use Python >= 3.10 for docs

---
 poetry.lock    | 72 +++++++++++++++++++++++++-------------------------
 pyproject.toml |  2 +-
 2 files changed, 37 insertions(+), 37 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index adcf5c59d..de15bc707 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,13 +2,13 @@
 
 [[package]]
 name = "alabaster"
-version = "0.7.16"
+version = "1.0.0"
 description = "A light, configurable Sphinx theme"
 optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
 files = [
-    {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"},
-    {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
+    {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"},
+    {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"},
 ]
 
 [[package]]
@@ -403,13 +403,13 @@ test-randomorder = ["pytest-randomly"]
 
 [[package]]
 name = "docutils"
-version = "0.20.1"
+version = "0.21.2"
 description = "Docutils -- Python Documentation Utilities"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
 files = [
-    {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"},
-    {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
+    {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"},
+    {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"},
 ]
 
 [[package]]
@@ -942,17 +942,17 @@ files = [
 
 [[package]]
 name = "readme-renderer"
-version = "43.0"
+version = "44.0"
 description = "readme_renderer is a library for rendering readme descriptions for Warehouse"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"},
-    {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"},
+    {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"},
+    {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"},
 ]
 
 [package.dependencies]
-docutils = ">=0.13.1"
+docutils = ">=0.21.2"
 nh3 = ">=0.2.14"
 Pygments = ">=2.5.1"
 
@@ -1066,38 +1066,38 @@ files = [
 
 [[package]]
 name = "sphinx"
-version = "7.1.2"
+version = "8.1.3"
 description = "Python documentation generator"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
 files = [
-    {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
-    {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
+    {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
+    {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
 ]
 
 [package.dependencies]
-alabaster = ">=0.7,<0.8"
-babel = ">=2.9"
-colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
-docutils = ">=0.18.1,<0.21"
+alabaster = ">=0.7.14"
+babel = ">=2.13"
+colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
+docutils = ">=0.20,<0.22"
 imagesize = ">=1.3"
-importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
-Jinja2 = ">=3.0"
-packaging = ">=21.0"
-Pygments = ">=2.13"
-requests = ">=2.25.0"
-snowballstemmer = ">=2.0"
-sphinxcontrib-applehelp = "*"
-sphinxcontrib-devhelp = "*"
-sphinxcontrib-htmlhelp = ">=2.0.0"
-sphinxcontrib-jsmath = "*"
-sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = ">=1.1.5"
+Jinja2 = ">=3.1"
+packaging = ">=23.0"
+Pygments = ">=2.17"
+requests = ">=2.30.0"
+snowballstemmer = ">=2.2"
+sphinxcontrib-applehelp = ">=1.0.7"
+sphinxcontrib-devhelp = ">=1.0.6"
+sphinxcontrib-htmlhelp = ">=2.0.6"
+sphinxcontrib-jsmath = ">=1.0.1"
+sphinxcontrib-qthelp = ">=1.0.6"
+sphinxcontrib-serializinghtml = ">=1.1.9"
+tomli = {version = ">=2", markers = "python_version < \"3.11\""}
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
-test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
+lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
+test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
 
 [[package]]
 name = "sphinx-basic-ng"
@@ -1327,4 +1327,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "4daba05b7366ab3731b6254c80109031f8b027f3d10127a41bc739f0cb776755"
+content-hash = "b1451f3a657a7a8be43a5f48a3c47af4b56ebf312cc19da9e2230af3973f8fc3"
diff --git a/pyproject.toml b/pyproject.toml
index f9b5f0ee7..21c3c69d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,5 +28,5 @@ sshuttle = "sshuttle.cmdline:main"
 optional = true
 
 [tool.poetry.group.docs.dependencies]
-sphinx = "7.1.2"
+sphinx = { version = "8.1.3", python = ">=3.10,<4.0" }
 furo = "2024.8.6"

From f7f9a4dbc67c76e3967393d9002181be28b43e19 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 6 Feb 2025 10:04:34 +0000
Subject: [PATCH 245/275] build(deps): bump actions/cache from 3 to 4

Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 .github/workflows/pythonpackage.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 1eeacdef3..c7648964d 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -32,7 +32,7 @@ jobs:
       run: |
         poetry config virtualenvs.create true --local
         poetry config virtualenvs.in-project true --local
-    - uses: actions/cache@v3
+    - uses: actions/cache@v4
       name: Define a cache for the virtual environment based on the dependencies lock file
       with:
         path: ./.venv

From 3f0f88eb097d728c8aa95135442d8f9496da3306 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 6 Feb 2025 10:04:38 +0000
Subject: [PATCH 246/275] build(deps): bump abatilo/actions-poetry from 3 to 4

Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 3 to 4.
- [Release notes](https://github.com/abatilo/actions-poetry/releases)
- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc)
- [Commits](https://github.com/abatilo/actions-poetry/compare/v3...v4)

---
updated-dependencies:
- dependency-name: abatilo/actions-poetry
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 .github/workflows/pythonpackage.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index c7648964d..1108d36f0 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -25,7 +25,7 @@ jobs:
       with:
         python-version: ${{ matrix.python-version }}
     - name: Run image
-      uses: abatilo/actions-poetry@v3
+      uses: abatilo/actions-poetry@v4
       with:
         poetry-version: ${{ matrix.poetry-version }}
     - name: Setup a local virtual environment (if no poetry.toml file)

From d910b64be77fd7ef2a5f169b780bfda95e67318d Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 08:33:14 +1100
Subject: [PATCH 247/275] feat: Add release-please to build workflow

---
 .github/workflows/release-please.yml | 31 ++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 .github/workflows/release-please.yml

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 000000000..cdb8e8a6d
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,31 @@
+on:
+  push:
+    branches:
+      - master
+
+permissions:
+  contents: write
+  pull-requests: write
+
+name: release-please
+
+jobs:
+  release-please:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: googleapis/release-please-action@v4
+        with:
+          token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
+          release-type: python
+  upload-pypi:
+    name: Upload to pypi
+    needs: [release-please]
+    if: ${{ needs.release_please.outputs.release_created == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: Publish PyPi package
+        uses: code-specialist/pypi-poetry-publish@v1
+        with:
+          ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          PUBLISH_REGISTRY: "https://test.pypi.org/simple/"
+          PUBLISH_REGISTRY_PASSWORD: ${{ secrets.PYPI_TOKEN }}

From 75faa9b9e8a31cd09cc77feb8c16a44383fb2093 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 08:53:02 +1100
Subject: [PATCH 248/275] build: remove setup.py

---
 pyproject.toml | 12 ++++++++++
 setup.py       | 63 --------------------------------------------------
 2 files changed, 12 insertions(+), 63 deletions(-)
 delete mode 100755 setup.py

diff --git a/pyproject.toml b/pyproject.toml
index 21c3c69d7..5f8b9b511 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,18 @@ description = "Transparent proxy server that works as a poor man's VPN. Forwards
 authors = ["Brian May "]
 license = "LGPL-2.1"
 readme = "README.rst"
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "Intended Audience :: End Users/Desktop",
+    "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: System :: Networking",
+]
 
 [tool.poetry.dependencies]
 python = "^3.9"
diff --git a/setup.py b/setup.py
deleted file mode 100755
index 98af52d6b..000000000
--- a/setup.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2012-2014 Brian May
-#
-# This file is part of sshuttle.
-#
-# sshuttle is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 2.1 of
-# the License, or (at your option) any later version.
-#
-# sshuttle is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with sshuttle; If not, see .
-
-from setuptools import setup, find_packages
-
-
-setup(
-    name="sshuttle",
-    version='1.2.0',
-    url='https://github.com/sshuttle/sshuttle',
-    author='Brian May',
-    author_email='brian@linuxpenguins.xyz',
-    description='Full-featured" VPN over an SSH tunnel',
-    packages=find_packages(),
-    license="LGPL2.1+",
-    long_description=open('README.rst').read(),
-    long_description_content_type="text/x-rst",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "Intended Audience :: End Users/Desktop",
-        "License :: OSI Approved :: " +
-            "GNU Lesser General Public License v2 or later (LGPLv2+)",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: System :: Networking",
-    ],
-    entry_points={
-        'console_scripts': [
-            'sshuttle = sshuttle.cmdline:main',
-        ],
-    },
-    python_requires='>=3.9',
-    install_requires=[
-        "pydivert; os_name=='nt'"
-    ],
-    tests_require=[
-        'pytest',
-        'pytest-cov',
-        'pytest-runner',
-        'flake8',
-    ],
-    keywords="ssh vpn",
-)

From 7725f93d949d7c8239dd1c8783cd6b638c53920c Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 09:18:45 +1100
Subject: [PATCH 249/275] build: release to prod pypi

---
 .github/workflows/release-please.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index cdb8e8a6d..8e0a3d830 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -27,5 +27,4 @@ jobs:
         uses: code-specialist/pypi-poetry-publish@v1
         with:
           ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          PUBLISH_REGISTRY: "https://test.pypi.org/simple/"
           PUBLISH_REGISTRY_PASSWORD: ${{ secrets.PYPI_TOKEN }}

From c09e2985f20eea797a8cfa5d89c8880a66489598 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 08:42:40 +1100
Subject: [PATCH 250/275] chore(master): release 1.2.0

---
 CHANGELOG.md   | 24 ++++++++++++++++++++++++
 pyproject.toml |  2 +-
 2 files changed, 25 insertions(+), 1 deletion(-)
 create mode 100644 CHANGELOG.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..3564ef73c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Changelog
+
+## [1.2.0](https://github.com/sshuttle/sshuttle/compare/v1.1.2...v1.2.0) (2025-02-07)
+
+
+### Features
+
+* Add release-please to build workflow ([d910b64](https://github.com/sshuttle/sshuttle/commit/d910b64be77fd7ef2a5f169b780bfda95e67318d))
+
+
+### Bug Fixes
+
+* Add support for Python 3.11 and Python 3.11 ([a3396a4](https://github.com/sshuttle/sshuttle/commit/a3396a443df14d3bafc3d25909d9221aa182b8fc))
+* bad file descriptor error in windows, fix pytest errors ([d4d0fa9](https://github.com/sshuttle/sshuttle/commit/d4d0fa945d50606360aa7c5f026a0f190b026c68))
+* drop Python 3.8 support ([1084c0f](https://github.com/sshuttle/sshuttle/commit/1084c0f2458c1595b00963b3bd54bd667e4cfc9f))
+* ensure poetry works for Python 3.9 ([693ee40](https://github.com/sshuttle/sshuttle/commit/693ee40c485c70f353326eb0e8f721f984850f5c))
+* fix broken workflow_dispatch CI rule ([4b6f7c6](https://github.com/sshuttle/sshuttle/commit/4b6f7c6a656a752552295863092d3b8af0b42b31))
+* Remove more references to legacy Python versions ([339b522](https://github.com/sshuttle/sshuttle/commit/339b5221bc33254329f79f2374f6114be6f30aed))
+* replace requirements.txt files with poetry ([85dc319](https://github.com/sshuttle/sshuttle/commit/85dc3199a332f9f9f0e4c6037c883a8f88dc09ca))
+* replace requirements.txt files with poetry (2) ([d08f78a](https://github.com/sshuttle/sshuttle/commit/d08f78a2d9777951d7e18f6eaebbcdd279d7683a))
+* replace requirements.txt files with poetry (3) ([62da705](https://github.com/sshuttle/sshuttle/commit/62da70510e8a1f93e8b38870fdebdbace965cd8e))
+* replace requirements.txt files with poetry (4) ([9bcedf1](https://github.com/sshuttle/sshuttle/commit/9bcedf19049e5b3a8ae26818299cc518ec03a926))
+* update nix flake to fix problems ([cda60a5](https://github.com/sshuttle/sshuttle/commit/cda60a52331c7102cff892b9b77c8321e276680a))
+* use Python >= 3.10 for docs ([bf29464](https://github.com/sshuttle/sshuttle/commit/bf294643e283cef9fb285d44e307e958686caf46))
diff --git a/pyproject.toml b/pyproject.toml
index 5f8b9b511..da62877bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "sshuttle"
-version = "1.1.2"
+version = "1.2.0"
 description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
 authors = ["Brian May "]
 license = "LGPL-2.1"

From 40f6c1d4f2205e82aa7a1a724a3415d284217df2 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 09:22:48 +1100
Subject: [PATCH 251/275] build: don't skip pypi release

---
 .github/workflows/release-please.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 8e0a3d830..97419efad 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -20,7 +20,7 @@ jobs:
   upload-pypi:
     name: Upload to pypi
     needs: [release-please]
-    if: ${{ needs.release_please.outputs.release_created == 'true' }}
+    if: ${{ needs.release-please.outputs.release_created == 'true' }}
     runs-on: ubuntu-latest
     steps:
       - name: Publish PyPi package

From 5b08caaeb16cec9995c0f16bd4425368d32bcb03 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 09:25:06 +1100
Subject: [PATCH 252/275] build: hack force publish pypi

---
 .github/workflows/release-please.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 97419efad..2766600ee 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -20,7 +20,7 @@ jobs:
   upload-pypi:
     name: Upload to pypi
     needs: [release-please]
-    if: ${{ needs.release-please.outputs.release_created == 'true' }}
+    # if: ${{ needs.release-please.outputs.release_created == 'true' }}
     runs-on: ubuntu-latest
     steps:
       - name: Publish PyPi package

From fd6b6bb71fd2270c13352b22c124b9d6f4cb571b Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 09:36:48 +1100
Subject: [PATCH 253/275] build: redo publish to pypi

---
 .github/workflows/release-please.yml | 43 ++++++++++++++++++++++------
 README.rst                           |  1 -
 2 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 2766600ee..86790339c 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -3,28 +3,53 @@ on:
     branches:
       - master
 
-permissions:
-  contents: write
-  pull-requests: write
-
 name: release-please
 
 jobs:
+
   release-please:
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
     steps:
       - uses: googleapis/release-please-action@v4
         with:
           token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
           release-type: python
+
   upload-pypi:
     name: Upload to pypi
     needs: [release-please]
     # if: ${{ needs.release-please.outputs.release_created == 'true' }}
     runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/sshuttle
+    permissions:
+      id-token: write
     steps:
-      - name: Publish PyPi package
-        uses: code-specialist/pypi-poetry-publish@v1
-        with:
-          ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          PUBLISH_REGISTRY_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+    - uses: actions/checkout@v4
+    - name: Set up Python 3.12
+      uses: actions/setup-python@v5
+      with:
+        python-version: 3.12
+    - name: Run image
+      uses: abatilo/actions-poetry@v4
+      with:
+        poetry-version: main
+    - name: Setup a local virtual environment (if no poetry.toml file)
+      run: |
+        poetry config virtualenvs.create true --local
+        poetry config virtualenvs.in-project true --local
+    - uses: actions/cache@v4
+      name: Define a cache for the virtual environment based on the dependencies lock file
+      with:
+        path: ./.venv
+        key: venv-${{ hashFiles('poetry.lock') }}
+    - name: Install the project dependencies
+      run: poetry install
+    - name: Package project
+      run: poetry build
+    - name: Publish package distributions to PyPI
+      uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/README.rst b/README.rst
index d268ed2da..73f12146c 100644
--- a/README.rst
+++ b/README.rst
@@ -109,7 +109,6 @@ It is also possible to install into a virtualenv as a non-root user.
       nix-env -iA nixpkgs.sshuttle
 
 - Windows::
-Use PyPI 
 
       pip install sshuttle
 

From 1f5e6cea703db33761fb1c3f999b9624cf3bc7ad Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 09:59:46 +1100
Subject: [PATCH 254/275] fix: remove temp build hack

---
 .github/workflows/release-please.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 86790339c..3177c27b8 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -21,7 +21,7 @@ jobs:
   upload-pypi:
     name: Upload to pypi
     needs: [release-please]
-    # if: ${{ needs.release-please.outputs.release_created == 'true' }}
+    if: ${{ needs.release-please.outputs.release_created == 'true' }}
     runs-on: ubuntu-latest
     environment:
       name: pypi

From 340ccc705ebd9499f14f799fcef0b5d2a8055fb4 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 8 Feb 2025 10:02:31 +1100
Subject: [PATCH 255/275] docs: replace nix-env with nix-shell

---
 README.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.rst b/README.rst
index 73f12146c..afd8ed266 100644
--- a/README.rst
+++ b/README.rst
@@ -106,7 +106,7 @@ It is also possible to install into a virtualenv as a non-root user.
 
 - Nix::
 
-      nix-env -iA nixpkgs.sshuttle
+      nix-shell -p sshuttle
 
 - Windows::
 

From cbe3d1e402cac9d3fbc818fe0cb8a87be2e94348 Mon Sep 17 00:00:00 2001
From: jraylan 
Date: Sat, 8 Feb 2025 18:45:25 -0300
Subject: [PATCH 256/275] fix: prevent UnicodeDecodeError parsing iptables rule
 with comments

If one or more iptables rule contains a comment with a non-unicode character, an UnicodeDecodeError would be raised.
---
 sshuttle/linux.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshuttle/linux.py b/sshuttle/linux.py
index 5055fc03d..ea5f954c2 100644
--- a/sshuttle/linux.py
+++ b/sshuttle/linux.py
@@ -20,7 +20,7 @@ def ipt_chain_exists(family, table, name):
     argv = [cmd, '-w', '-t', table, '-nL']
     try:
         output = ssubprocess.check_output(argv, env=get_env())
-        for line in output.decode('ASCII').split('\n'):
+        for line in output.decode('ASCII', errors='replace').split('\n'):
             if line.startswith('Chain %s ' % name):
                 return True
     except ssubprocess.CalledProcessError as e:

From 8a123d9762b84f168a8ca8c75f73e590954e122d Mon Sep 17 00:00:00 2001
From: jraylan 
Date: Sat, 8 Feb 2025 18:48:55 -0300
Subject: [PATCH 257/275] feat: switch to a network namespace on Linux

* Add support to run inside Linux namespace

**Motivation:**
In a specific use case, we use sshuttle to provide access to private
networks from multiple sites to a specific host. The sites may contain
networks that overlap each other, so each site is accessed inside a
different namespace that provides process-level network isolation and
prevents network overlap.

**Objective:**
This commit just adds a convenient way of spawning multiple sshuttle
instances inside different namespaces from a single process, by passing
the namespace's name though the variable --namespace. The result is the
same as calling `ip netns exec $NAMESPACE sshuttle ...`

* Add the argument --namespace-pid

The argument '--namespace-pid' allows sshuttle to attach to the same net
namespace used by a running process.

* PEP-8 compliance

* Add comment

* Make --namespace and --namespace-pid mutually exclusive.

* Prevent UnicodeDecodeError parsing iptables rule with comments

If one or more iptables rule contains a comment with a non-unicode character, an UnicodeDecodeError would be raised.
---
 sshuttle/cmdline.py          | 11 ++++++++++
 sshuttle/namespace.py        | 40 ++++++++++++++++++++++++++++++++++++
 sshuttle/options.py          | 26 +++++++++++++++++++++++
 tests/client/test_options.py | 30 +++++++++++++++++++++++++++
 4 files changed, 107 insertions(+)
 create mode 100644 sshuttle/namespace.py

diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py
index 11d679642..94ee58272 100644
--- a/sshuttle/cmdline.py
+++ b/sshuttle/cmdline.py
@@ -11,6 +11,7 @@
 from sshuttle.options import parser, parse_ipport
 from sshuttle.helpers import family_ip_tuple, log, Fatal
 from sshuttle.sudoers import sudoers
+from sshuttle.namespace import enter_namespace
 
 
 def main():
@@ -37,6 +38,16 @@ def main():
     helpers.verbose = opt.verbose
 
     try:
+        # Since namespace and namespace-pid options are only available
+        # in linux, we must check if it exists with getattr
+        namespace = getattr(opt, 'namespace', None)
+        namespace_pid = getattr(opt, 'namespace_pid', None)
+        if namespace or namespace_pid:
+            prefix = helpers.logprefix
+            helpers.logprefix = 'ns: '
+            enter_namespace(namespace, namespace_pid)
+            helpers.logprefix = prefix
+
         if opt.firewall:
             if opt.subnets or opt.subnets_file:
                 parser.error('exactly zero arguments expected')
diff --git a/sshuttle/namespace.py b/sshuttle/namespace.py
new file mode 100644
index 000000000..f168b747b
--- /dev/null
+++ b/sshuttle/namespace.py
@@ -0,0 +1,40 @@
+import os
+import ctypes
+import ctypes.util
+
+from sshuttle.helpers import Fatal, debug1, debug2
+
+
+CLONE_NEWNET = 0x40000000
+NETNS_RUN_DIR = "/var/run/netns"
+
+
+def enter_namespace(namespace, namespace_pid):
+    if namespace:
+        namespace_dir = f'{NETNS_RUN_DIR}/{namespace}'
+    else:
+        namespace_dir = f'/proc/{namespace_pid}/ns/net'
+
+    if not os.path.exists(namespace_dir):
+        raise Fatal('The namespace %r does not exists.' % namespace_dir)
+
+    debug2('loading libc')
+    libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+
+    default_errcheck = libc.setns.errcheck
+
+    def errcheck(ret, *args):
+        if ret == -1:
+            e = ctypes.get_errno()
+            raise Fatal(e, os.strerror(e))
+        if default_errcheck:
+            return default_errcheck(ret, *args)
+
+    libc.setns.errcheck = errcheck  # type: ignore
+
+    debug1('Entering namespace %r' % namespace_dir)
+
+    with open(namespace_dir) as fd:
+        libc.setns(fd.fileno(), CLONE_NEWNET)
+
+    debug1('Namespace %r successfully set' % namespace_dir)
diff --git a/sshuttle/options.py b/sshuttle/options.py
index b610a11dc..ca4993340 100644
--- a/sshuttle/options.py
+++ b/sshuttle/options.py
@@ -137,6 +137,15 @@ def parse_list(lst):
     return re.split(r'[\s,]+', lst.strip()) if lst else []
 
 
+def parse_namespace(namespace):
+    try:
+        assert re.fullmatch(
+            r'(@?[a-z_A-Z]\w+(?:\.@?[a-z_A-Z]\w+)*)', namespace)
+        return namespace
+    except AssertionError:
+        raise Fatal("%r is not a valid namespace name." % namespace)
+
+
 class Concat(Action):
     def __init__(self, option_strings, dest, nargs=None, **kwargs):
         if nargs is not None:
@@ -460,3 +469,20 @@ def convert_arg_line_to_args(self, arg_line):
     hexadecimal (default '0x01')
     """
 )
+
+if sys.platform == 'linux':
+    net_ns_group = parser.add_mutually_exclusive_group(
+        required=False)
+
+    net_ns_group.add_argument(
+        '--namespace',
+        type=parse_namespace,
+        help="Run inside of a net namespace with the given name."
+    )
+    net_ns_group.add_argument(
+        '--namespace-pid',
+        type=int,
+        help="""
+        Run inside the net namespace used by the process with
+        the given pid."""
+    )
diff --git a/tests/client/test_options.py b/tests/client/test_options.py
index fe0411703..0bb6d79c9 100644
--- a/tests/client/test_options.py
+++ b/tests/client/test_options.py
@@ -176,3 +176,33 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo):
             (socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90),
             (socket.AF_INET, '142.251.42.129', 32, 80, 90),
         ])
+
+
+def test_parse_namespace():
+    valid_namespaces = [
+        'my_namespace',
+        'my.namespace',
+        'my_namespace_with_underscore',
+        'MyNamespace',
+        '@my_namespace',
+        'my.long_namespace.with.multiple.dots',
+        '@my.long_namespace.with.multiple.dots',
+        'my.Namespace.With.Mixed.Case',
+    ]
+
+    for namespace in valid_namespaces:
+        assert sshuttle.options.parse_namespace(namespace) == namespace
+
+    invalid_namespaces = [
+        '',
+        '123namespace',
+        'my-namespace',
+        'my_namespace!',
+        '.my_namespace',
+        'my_namespace.',
+        'my..namespace',
+    ]
+
+    for namespace in invalid_namespaces:
+        with pytest.raises(Fatal, match="'.*' is not a valid namespace name."):
+            sshuttle.options.parse_namespace(namespace)

From a1dd6859b0adde6627a47b9bf5ff49d888e831e8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 Feb 2025 10:59:30 +0000
Subject: [PATCH 258/275] build(deps-dev): bump flake8 from 7.1.1 to 7.1.2

Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.1 to 7.1.2.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.1...7.1.2)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 poetry.lock | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index de15bc707..c55a83bad 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -428,13 +428,13 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "flake8"
-version = "7.1.1"
+version = "7.1.2"
 description = "the modular source code checker: pep8 pyflakes and co"
 optional = false
 python-versions = ">=3.8.1"
 files = [
-    {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
-    {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
+    {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"},
+    {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"},
 ]
 
 [package.dependencies]

From 7fa927ef8ceea6b1b2848ca433b8b3e3b63f0509 Mon Sep 17 00:00:00 2001
From: Christian Schlotter 
Date: Fri, 21 Feb 2025 22:23:36 +0100
Subject: [PATCH 259/275] fix: support ':' sign in password

---
 sshuttle/ssh.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py
index 8f295e214..c4e417ecc 100644
--- a/sshuttle/ssh.py
+++ b/sshuttle/ssh.py
@@ -56,7 +56,7 @@ def parse_hostport(rhostport):
         # Fix #410 bad username error detect
         if ":" in username:
             # this will even allow for the username to be empty
-            username, password = username.split(":")
+            username, password = username.split(":", 1)
 
     if ":" in host:
         # IPv6 address and/or got a port specified

From a2d405a6a7f9d1a301311a109f8411f2fe8deb37 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sat, 22 Feb 2025 08:45:54 +1100
Subject: [PATCH 260/275] docs: update installation instructions

* Update pip installation instructions to work
  without setup.py.

* Remove duplication of installation instructions
  in two places.
---
 README.rst            | 82 ++-----------------------------------------
 docs/installation.rst | 75 +++++++++++++++++++++++++++++++++++----
 2 files changed, 70 insertions(+), 87 deletions(-)

diff --git a/README.rst b/README.rst
index afd8ed266..6cf500a24 100644
--- a/README.rst
+++ b/README.rst
@@ -30,87 +30,9 @@ common case:
 Obtaining sshuttle
 ------------------
 
-- Ubuntu 16.04 or later::
+Please see the documentation_.
 
-      apt-get install sshuttle
-
-- Debian stretch or later::
-
-      apt-get install sshuttle
-
-- Arch Linux::
-
-      pacman -S sshuttle
-
-- Fedora::
-
-      dnf install sshuttle
-
-- openSUSE::
-
-      zypper in sshuttle
-
-- Gentoo::
-
-      emerge -av net-proxy/sshuttle
-
-- NixOS::
-
-      nix-env -iA nixos.sshuttle
-
-- From PyPI::
-
-      sudo pip install sshuttle
-
-- Clone::
-
-      git clone https://github.com/sshuttle/sshuttle.git
-      cd sshuttle
-      sudo ./setup.py install
-
-- FreeBSD::
-
-      # ports
-      cd /usr/ports/net/py-sshuttle && make install clean
-      # pkg
-      pkg install py39-sshuttle
-
-- OpenBSD::
-
-      pkg_add sshuttle
-
-- macOS, via MacPorts::
-
-      sudo port selfupdate
-      sudo port install sshuttle
-
-It is also possible to install into a virtualenv as a non-root user.
-
-- From PyPI::
-
-      virtualenv -p python3 /tmp/sshuttle
-      . /tmp/sshuttle/bin/activate
-      pip install sshuttle
-
-- Clone::
-
-      virtualenv -p python3 /tmp/sshuttle
-      . /tmp/sshuttle/bin/activate
-      git clone https://github.com/sshuttle/sshuttle.git
-      cd sshuttle
-      ./setup.py install
-
-- Homebrew::
-
-      brew install sshuttle
-
-- Nix::
-
-      nix-shell -p sshuttle
-
-- Windows::
-
-      pip install sshuttle
+.. _Documentation: https://sshuttle.readthedocs.io/en/stable/installation.html
 
 Documentation
 -------------
diff --git a/docs/installation.rst b/docs/installation.rst
index 97c62a8e2..0d56594ab 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -1,23 +1,84 @@
 Installation
 ============
 
+- Ubuntu 16.04 or later::
+
+      apt-get install sshuttle
+
+- Debian stretch or later::
+
+      apt-get install sshuttle
+
+- Arch Linux::
+
+      pacman -S sshuttle
+
+- Fedora::
+
+      dnf install sshuttle
+
+- openSUSE::
+
+      zypper in sshuttle
+
+- Gentoo::
+
+      emerge -av net-proxy/sshuttle
+
+- NixOS::
+
+      nix-env -iA nixos.sshuttle
+
 - From PyPI::
 
-      pip install sshuttle
+      sudo pip install sshuttle
+
+- Clone::
+
+      git clone https://github.com/sshuttle/sshuttle.git
+      cd sshuttle
+      sudo ./setup.py install
+
+- FreeBSD::
+
+      # ports
+      cd /usr/ports/net/py-sshuttle && make install clean
+      # pkg
+      pkg install py39-sshuttle
+
+- OpenBSD::
+
+      pkg_add sshuttle
 
-- Debian package manager::
+- macOS, via MacPorts::
 
-      sudo apt install sshuttle
+      sudo port selfupdate
+      sudo port install sshuttle
+
+It is also possible to install into a virtualenv as a non-root user.
+
+- From PyPI::
+
+      python3 -m venv /tmp/sshuttle
+      . /tmp/sshuttle/bin/activate
+      pip install sshuttle
 
 - Clone::
 
       git clone https://github.com/sshuttle/sshuttle.git
       cd sshuttle
-      ./setup.py install
+      python3 -m venv /tmp/sshuttle
+      . /tmp/sshuttle/bin/activate
+      python -m pip install .
+
+- Homebrew::
 
+      brew install sshuttle
 
-Optionally after installation
------------------------------
+- Nix::
 
-- Install sudoers configuration. For details, see the "Sudoers File" section in :doc:`usage`
+      nix-shell -p sshuttle
 
+- Windows::
+
+      pip install sshuttle

From 99c4abce812cf533421a432b7b33882d21e98150 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sun, 23 Feb 2025 20:38:38 +1100
Subject: [PATCH 261/275] chore(master): release 1.3.0

---
 CHANGELOG.md   | 20 ++++++++++++++++++++
 pyproject.toml |  2 +-
 setup.cfg      |  2 +-
 3 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3564ef73c..d6f3951c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Changelog
 
+## [1.3.0](https://github.com/sshuttle/sshuttle/compare/v1.2.0...v1.3.0) (2025-02-23)
+
+
+### Features
+
+* switch to a network namespace on Linux ([8a123d9](https://github.com/sshuttle/sshuttle/commit/8a123d9762b84f168a8ca8c75f73e590954e122d))
+
+
+### Bug Fixes
+
+* prevent UnicodeDecodeError parsing iptables rule with comments ([cbe3d1e](https://github.com/sshuttle/sshuttle/commit/cbe3d1e402cac9d3fbc818fe0cb8a87be2e94348))
+* remove temp build hack ([1f5e6ce](https://github.com/sshuttle/sshuttle/commit/1f5e6cea703db33761fb1c3f999b9624cf3bc7ad))
+* support ':' sign in password ([7fa927e](https://github.com/sshuttle/sshuttle/commit/7fa927ef8ceea6b1b2848ca433b8b3e3b63f0509))
+
+
+### Documentation
+
+* replace nix-env with nix-shell ([340ccc7](https://github.com/sshuttle/sshuttle/commit/340ccc705ebd9499f14f799fcef0b5d2a8055fb4))
+* update installation instructions ([a2d405a](https://github.com/sshuttle/sshuttle/commit/a2d405a6a7f9d1a301311a109f8411f2fe8deb37))
+
 ## [1.2.0](https://github.com/sshuttle/sshuttle/compare/v1.1.2...v1.2.0) (2025-02-07)
 
 
diff --git a/pyproject.toml b/pyproject.toml
index da62877bd..c69536be7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "sshuttle"
-version = "1.2.0"
+version = "1.3.0"
 description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
 authors = ["Brian May "]
 license = "LGPL-2.1"
diff --git a/setup.cfg b/setup.cfg
index 7f47b3dd1..69d69e405 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 1.2.0
+current_version = 1.3.0
 
 [bumpversion:file:setup.py]
 

From 7991e3d9a2eabb0647ec29b4d86ab26631dfa538 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Mon, 24 Feb 2025 08:13:46 +1100
Subject: [PATCH 262/275] build: fix pypi upload getting skipped

---
 .github/workflows/release-please.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 3177c27b8..c22db4ff1 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -12,8 +12,12 @@ jobs:
     permissions:
       contents: write
       pull-requests: write
+    outputs:
+      release_created: ${{ steps.release.outputs.release_created }}
+      tag_name: ${{ steps.release.outputs.tag_name }}
     steps:
       - uses: googleapis/release-please-action@v4
+        id: release
         with:
           token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
           release-type: python

From 12138e2b8d13503734351d885ac1e2d1c6716e02 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sun, 2 Mar 2025 17:17:42 +1100
Subject: [PATCH 263/275] build: split build and upload into 2 jobs

---
 .github/workflows/release-please.yml | 30 +++++++++++++++++++++-------
 1 file changed, 23 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index c22db4ff1..1b92c9cda 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -22,16 +22,11 @@ jobs:
           token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
           release-type: python
 
-  upload-pypi:
-    name: Upload to pypi
+  build-pypi:
+    name: Build for pypi
     needs: [release-please]
     if: ${{ needs.release-please.outputs.release_created == 'true' }}
     runs-on: ubuntu-latest
-    environment:
-      name: pypi
-      url: https://pypi.org/p/sshuttle
-    permissions:
-      id-token: write
     steps:
     - uses: actions/checkout@v4
     - name: Set up Python 3.12
@@ -55,5 +50,26 @@ jobs:
       run: poetry install
     - name: Package project
       run: poetry build
+    - name: Store the distribution packages
+      uses: actions/upload-artifact@v4
+      with:
+        name: python-package-distributions
+        path: dist/
+
+  upload-pypi:
+    name: Upload to pypi
+    needs: [build-pypi]
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/sshuttle
+    permissions:
+      id-token: write
+    steps:
+    - name: Download all the dists
+      uses: actions/download-artifact@v4
+      with:
+        name: python-package-distributions
+        path: dist/
     - name: Publish package distributions to PyPI
       uses: pypa/gh-action-pypi-publish@release/v1

From 0b7440e65c8f7b2b83df8fc245f330a34f611dd3 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sun, 2 Mar 2025 19:03:53 +1100
Subject: [PATCH 264/275] build: convert from poetry to uv

---
 .github/workflows/pythonpackage.yml  |   26 +-
 .github/workflows/release-please.yml |   23 +-
 .readthedocs.yaml                    |    5 +-
 flake.lock                           |  135 +--
 flake.nix                            |  116 ++-
 poetry.lock                          | 1330 --------------------------
 pyproject.toml                       |   54 +-
 uv.lock                              | 1061 ++++++++++++++++++++
 8 files changed, 1243 insertions(+), 1507 deletions(-)
 delete mode 100644 poetry.lock
 create mode 100644 uv.lock

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 1108d36f0..4d0130ad9 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -24,23 +24,15 @@ jobs:
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
-    - name: Run image
-      uses: abatilo/actions-poetry@v4
+    - name: Install uv
+      uses: astral-sh/setup-uv@v5
       with:
-        poetry-version: ${{ matrix.poetry-version }}
-    - name: Setup a local virtual environment (if no poetry.toml file)
-      run: |
-        poetry config virtualenvs.create true --local
-        poetry config virtualenvs.in-project true --local
-    - uses: actions/cache@v4
-      name: Define a cache for the virtual environment based on the dependencies lock file
-      with:
-        path: ./.venv
-        key: venv-${{ hashFiles('poetry.lock') }}
-    - name: Install the project dependencies
-      run: poetry install
+        version: "0.4.30"
+        enable-cache: true
+        cache-dependency-glob: "uv.lock"
+    - name: Install the project
+      run: uv sync --all-extras --dev
     - name: Lint with flake8
-      run: |
-        poetry run flake8 sshuttle tests --count --show-source --statistics
+      run: uv run flake8 sshuttle tests --count --show-source --statistics
     - name: Run the automated tests
-      run: poetry run pytest -v
+      run: uv run pytest -v
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 1b92c9cda..d259f7a5a 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -33,23 +33,14 @@ jobs:
       uses: actions/setup-python@v5
       with:
         python-version: 3.12
-    - name: Run image
-      uses: abatilo/actions-poetry@v4
+    - name: Install uv
+      uses: astral-sh/setup-uv@v5
       with:
-        poetry-version: main
-    - name: Setup a local virtual environment (if no poetry.toml file)
-      run: |
-        poetry config virtualenvs.create true --local
-        poetry config virtualenvs.in-project true --local
-    - uses: actions/cache@v4
-      name: Define a cache for the virtual environment based on the dependencies lock file
-      with:
-        path: ./.venv
-        key: venv-${{ hashFiles('poetry.lock') }}
-    - name: Install the project dependencies
-      run: poetry install
-    - name: Package project
-      run: poetry build
+        version: "0.4.30"
+        enable-cache: true
+        cache-dependency-glob: "uv.lock"
+    - name: Build project
+      run: uv build
     - name: Store the distribution packages
       uses: actions/upload-artifact@v4
       with:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 0a16c54bc..6b94913ab 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -5,10 +5,9 @@ build:
   tools:
     python: "3.10"
   jobs:
-    post_create_environment:
-      - pip install poetry
     post_install:
-      - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs
+      - pip install uv
+      - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy
 
 sphinx:
   configuration: docs/conf.py
diff --git a/flake.lock b/flake.lock
index 1748fe433..640115a1b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -18,82 +18,65 @@
         "type": "github"
       }
     },
-    "flake-utils_2": {
-      "inputs": {
-        "systems": "systems_2"
-      },
+    "nixpkgs": {
       "locked": {
-        "lastModified": 1726560853,
-        "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
+        "lastModified": 1740743217,
+        "narHash": "sha256-brsCRzLqimpyhORma84c3W2xPbIidZlIc3JGIuQVSNI=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b27ba4eb322d9d2bf2dc9ada9fd59442f50c8d7c",
         "type": "github"
       },
       "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
+        "owner": "NixOS",
+        "ref": "nixos-24.11",
+        "repo": "nixpkgs",
         "type": "github"
       }
     },
-    "nix-github-actions": {
+    "pyproject-build-systems": {
       "inputs": {
         "nixpkgs": [
-          "poetry2nix",
           "nixpkgs"
+        ],
+        "pyproject-nix": [
+          "pyproject-nix"
+        ],
+        "uv2nix": [
+          "uv2nix"
         ]
       },
       "locked": {
-        "lastModified": 1729742964,
-        "narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
-        "owner": "nix-community",
-        "repo": "nix-github-actions",
-        "rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-community",
-        "repo": "nix-github-actions",
-        "type": "github"
-      }
-    },
-    "nixpkgs": {
-      "locked": {
-        "lastModified": 1738702386,
-        "narHash": "sha256-nJj8f78AYAxl/zqLiFGXn5Im1qjFKU8yBPKoWEeZN5M=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "030ba1976b7c0e1a67d9716b17308ccdab5b381e",
+        "lastModified": 1740362541,
+        "narHash": "sha256-S8Mno07MspggOv/xIz5g8hB2b/C5HPiX8E+rXzKY+5U=",
+        "owner": "pyproject-nix",
+        "repo": "build-system-pkgs",
+        "rev": "e151741c848ba92331af91f4e47640a1fb82be19",
         "type": "github"
       },
       "original": {
-        "owner": "NixOS",
-        "ref": "nixos-24.11",
-        "repo": "nixpkgs",
+        "owner": "pyproject-nix",
+        "repo": "build-system-pkgs",
         "type": "github"
       }
     },
-    "poetry2nix": {
+    "pyproject-nix": {
       "inputs": {
-        "flake-utils": "flake-utils_2",
-        "nix-github-actions": "nix-github-actions",
         "nixpkgs": [
           "nixpkgs"
-        ],
-        "systems": "systems_3",
-        "treefmt-nix": "treefmt-nix"
+        ]
       },
       "locked": {
-        "lastModified": 1738741221,
-        "narHash": "sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE=",
-        "owner": "nix-community",
-        "repo": "poetry2nix",
-        "rev": "be1fe795035d3d36359ca9135b26dcc5321b31fb",
+        "lastModified": 1739758351,
+        "narHash": "sha256-Aoa4dEoC7Hf6+gFVk/SDquZTMFlmlfsgdTWuqQxzePs=",
+        "owner": "pyproject-nix",
+        "repo": "pyproject.nix",
+        "rev": "1329712f7f9af3a8b270764ba338a455b7323811",
         "type": "github"
       },
       "original": {
-        "owner": "nix-community",
-        "repo": "poetry2nix",
+        "owner": "pyproject-nix",
+        "repo": "pyproject.nix",
         "type": "github"
       }
     },
@@ -101,7 +84,9 @@
       "inputs": {
         "flake-utils": "flake-utils",
         "nixpkgs": "nixpkgs",
-        "poetry2nix": "poetry2nix"
+        "pyproject-build-systems": "pyproject-build-systems",
+        "pyproject-nix": "pyproject-nix",
+        "uv2nix": "uv2nix"
       }
     },
     "systems": {
@@ -119,54 +104,26 @@
         "type": "github"
       }
     },
-    "systems_2": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
-    },
-    "systems_3": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
-    },
-    "treefmt-nix": {
+    "uv2nix": {
       "inputs": {
         "nixpkgs": [
-          "poetry2nix",
           "nixpkgs"
+        ],
+        "pyproject-nix": [
+          "pyproject-nix"
         ]
       },
       "locked": {
-        "lastModified": 1730120726,
-        "narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=",
-        "owner": "numtide",
-        "repo": "treefmt-nix",
-        "rev": "9ef337e492a5555d8e17a51c911ff1f02635be15",
+        "lastModified": 1740497536,
+        "narHash": "sha256-K+8wsVooqhaqyxuvew3+62mgOfRLJ7whv7woqPU3Ypo=",
+        "owner": "pyproject-nix",
+        "repo": "uv2nix",
+        "rev": "d01fd3a141755ad5d5b93dd9fcbd76d6401f5bac",
         "type": "github"
       },
       "original": {
-        "owner": "numtide",
-        "repo": "treefmt-nix",
+        "owner": "pyproject-nix",
+        "repo": "uv2nix",
         "type": "github"
       }
     }
diff --git a/flake.nix b/flake.nix
index f044ec532..2bdf72340 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,11 +1,24 @@
 {
   description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling.";
 
-  inputs.flake-utils.url = "github:numtide/flake-utils";
-  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
-  inputs.poetry2nix = {
-    url = "github:nix-community/poetry2nix";
-    inputs.nixpkgs.follows = "nixpkgs";
+  inputs = {
+    flake-utils.url = "github:numtide/flake-utils";
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
+    pyproject-nix = {
+      url = "github:pyproject-nix/pyproject.nix";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+    uv2nix = {
+      url = "github:pyproject-nix/uv2nix";
+      inputs.pyproject-nix.follows = "pyproject-nix";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+    pyproject-build-systems = {
+      url = "github:pyproject-nix/build-system-pkgs";
+      inputs.pyproject-nix.follows = "pyproject-nix";
+      inputs.uv2nix.follows = "uv2nix";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
   };
 
   outputs =
@@ -13,42 +26,87 @@
       self,
       nixpkgs,
       flake-utils,
-      poetry2nix,
+      pyproject-nix,
+      uv2nix,
+      pyproject-build-systems,
     }:
     flake-utils.lib.eachDefaultSystem (
       system:
       let
-        p2n = import poetry2nix { inherit pkgs; };
-        overrides = p2n.defaultPoetryOverrides.extend (
-          self: super: {
-            nh3 = super.nh3.override { preferWheel = true; };
-            bump2version = super.bump2version.overridePythonAttrs (old: {
-              buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ];
-            });
-          }
-        );
-
-        poetry_env = p2n.mkPoetryEnv {
-          python = pkgs.python3;
-          projectDir = self;
-          inherit overrides;
+        inherit (nixpkgs) lib;
+
+        pkgs = nixpkgs.legacyPackages.${system};
+
+        python = pkgs.python312;
+
+        workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
+
+        # Create package overlay from workspace.
+        overlay = workspace.mkPyprojectOverlay {
+          sourcePreference = "sdist";
         };
-        poetry_app = p2n.mkPoetryApplication {
-          python = pkgs.python3;
-          projectDir = self;
-          inherit overrides;
+
+        # Extend generated overlay with build fixups
+        #
+        # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds.
+        # This is an additional overlay implementing build fixups.
+        # See:
+        # - https://pyproject-nix.github.io/uv2nix/FAQ.html
+        pyprojectOverrides =
+          final: prev:
+          # Implement build fixups here.
+          # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage.
+          # It's using https://pyproject-nix.github.io/pyproject.nix/build.html
+          let
+            inherit (final) resolveBuildSystem;
+            inherit (builtins) mapAttrs;
+
+            # Build system dependencies specified in the shape expected by resolveBuildSystem
+            # The empty lists below are lists of optional dependencies.
+            #
+            # A package `foo` with specification written as:
+            # `setuptools-scm[toml]` in pyproject.toml would be written as
+            # `foo.setuptools-scm = [ "toml" ]` in Nix
+            buildSystemOverrides = {
+              chardet.setuptools = [ ];
+              colorlog.setuptools = [ ];
+              python-debian.setuptools = [ ];
+            };
+
+          in
+          mapAttrs (
+            name: spec:
+            prev.${name}.overrideAttrs (old: {
+              nativeBuildInputs = old.nativeBuildInputs ++ resolveBuildSystem spec;
+            })
+          ) buildSystemOverrides;
+
+        pythonSet =
+          (pkgs.callPackage pyproject-nix.build.packages {
+            inherit python;
+          }).overrideScope
+            (
+              lib.composeManyExtensions [
+                pyproject-build-systems.overlays.default
+                overlay
+                pyprojectOverrides
+              ]
+            );
+
+        inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication;
+        package = mkApplication {
+          venv = pythonSet.mkVirtualEnv "sshuttle" workspace.deps.default;
+          package = pythonSet.sshuttle;
         };
-        pkgs = nixpkgs.legacyPackages.${system};
       in
       {
         packages = {
-          sshuttle = poetry_app;
-          default = self.packages.${system}.sshuttle;
+          sshuttle = package;
+          default = package;
         };
         devShells.default = pkgs.mkShell {
           packages = [
-            pkgs.poetry
-            poetry_env
+            pkgs.uv
           ];
         };
       }
diff --git a/poetry.lock b/poetry.lock
deleted file mode 100644
index c55a83bad..000000000
--- a/poetry.lock
+++ /dev/null
@@ -1,1330 +0,0 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
-
-[[package]]
-name = "alabaster"
-version = "1.0.0"
-description = "A light, configurable Sphinx theme"
-optional = false
-python-versions = ">=3.10"
-files = [
-    {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"},
-    {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"},
-]
-
-[[package]]
-name = "babel"
-version = "2.17.0"
-description = "Internationalization utilities"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"},
-    {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"},
-]
-
-[package.extras]
-dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"]
-
-[[package]]
-name = "backports-tarfile"
-version = "1.2.0"
-description = "Backport of CPython tarfile module"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"},
-    {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
-
-[[package]]
-name = "beautifulsoup4"
-version = "4.13.3"
-description = "Screen-scraping library"
-optional = false
-python-versions = ">=3.7.0"
-files = [
-    {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"},
-    {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"},
-]
-
-[package.dependencies]
-soupsieve = ">1.2"
-typing-extensions = ">=4.0.0"
-
-[package.extras]
-cchardet = ["cchardet"]
-chardet = ["chardet"]
-charset-normalizer = ["charset-normalizer"]
-html5lib = ["html5lib"]
-lxml = ["lxml"]
-
-[[package]]
-name = "bump2version"
-version = "1.0.1"
-description = "Version-bump your software with a single command!"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"},
-    {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"},
-]
-
-[[package]]
-name = "certifi"
-version = "2025.1.31"
-description = "Python package for providing Mozilla's CA Bundle."
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
-    {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
-]
-
-[[package]]
-name = "cffi"
-version = "1.17.1"
-description = "Foreign Function Interface for Python calling C code."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
-    {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
-    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
-    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
-    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
-    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
-    {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
-    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
-    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
-    {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
-    {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
-    {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
-    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
-    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
-    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
-    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
-    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
-    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
-    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
-    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
-    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
-    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
-    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
-    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
-    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
-    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
-    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
-    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
-    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
-    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
-    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
-    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
-    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
-    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
-    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
-    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
-    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
-    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
-    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
-    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
-    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
-    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
-    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
-    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
-    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
-    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
-    {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
-    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
-    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
-    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
-    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
-    {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
-    {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
-    {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
-    {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
-    {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
-    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
-    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
-    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
-    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
-    {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
-    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
-    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
-    {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
-    {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
-    {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
-    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
-]
-
-[package.dependencies]
-pycparser = "*"
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.1"
-description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
-    {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
-    {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
-    {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
-    {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
-    {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
-    {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
-    {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
-    {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
-    {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-description = "Cross-platform colored terminal text."
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-files = [
-    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
-    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
-
-[[package]]
-name = "coverage"
-version = "7.6.10"
-description = "Code coverage measurement for Python"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
-    {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
-    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
-    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
-    {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
-    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
-    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
-    {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
-    {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
-    {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
-    {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
-    {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
-    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
-    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
-    {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
-    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
-    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
-    {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
-    {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
-    {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
-    {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
-    {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
-    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
-    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
-    {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
-    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
-    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
-    {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
-    {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
-    {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
-    {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
-    {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
-    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
-    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
-    {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
-    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
-    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
-    {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
-    {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
-    {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
-    {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
-    {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
-    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
-    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
-    {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
-    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
-    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
-    {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
-    {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
-    {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
-    {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
-    {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
-    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
-    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
-    {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
-    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
-    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
-    {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
-    {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
-    {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
-    {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
-    {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
-]
-
-[package.dependencies]
-tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
-
-[package.extras]
-toml = ["tomli"]
-
-[[package]]
-name = "cryptography"
-version = "43.0.3"
-description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
-    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
-    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
-    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
-    {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
-    {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
-    {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
-    {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
-    {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
-    {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
-    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
-    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
-    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
-    {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
-    {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
-    {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
-    {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
-    {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
-    {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
-    {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
-    {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
-    {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
-    {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
-    {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
-    {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
-    {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
-    {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
-]
-
-[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
-
-[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
-docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
-nox = ["nox"]
-pep8test = ["check-sdist", "click", "mypy", "ruff"]
-sdist = ["build"]
-ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
-test-randomorder = ["pytest-randomly"]
-
-[[package]]
-name = "docutils"
-version = "0.21.2"
-description = "Docutils -- Python Documentation Utilities"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"},
-    {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"},
-]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.2.2"
-description = "Backport of PEP 654 (exception groups)"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
-    {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
-]
-
-[package.extras]
-test = ["pytest (>=6)"]
-
-[[package]]
-name = "flake8"
-version = "7.1.2"
-description = "the modular source code checker: pep8 pyflakes and co"
-optional = false
-python-versions = ">=3.8.1"
-files = [
-    {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"},
-    {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"},
-]
-
-[package.dependencies]
-mccabe = ">=0.7.0,<0.8.0"
-pycodestyle = ">=2.12.0,<2.13.0"
-pyflakes = ">=3.2.0,<3.3.0"
-
-[[package]]
-name = "furo"
-version = "2024.8.6"
-description = "A clean customisable Sphinx documentation theme."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"},
-    {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"},
-]
-
-[package.dependencies]
-beautifulsoup4 = "*"
-pygments = ">=2.7"
-sphinx = ">=6.0,<9.0"
-sphinx-basic-ng = ">=1.0.0.beta2"
-
-[[package]]
-name = "id"
-version = "1.5.0"
-description = "A tool for generating OIDC identities"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"},
-    {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"},
-]
-
-[package.dependencies]
-requests = "*"
-
-[package.extras]
-dev = ["build", "bump (>=1.3.2)", "id[lint,test]"]
-lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"]
-test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"]
-
-[[package]]
-name = "idna"
-version = "3.10"
-description = "Internationalized Domain Names in Applications (IDNA)"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
-    {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
-]
-
-[package.extras]
-all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
-
-[[package]]
-name = "imagesize"
-version = "1.4.1"
-description = "Getting image size from png/jpeg/jpeg2000/gif file"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
-    {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
-    {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
-]
-
-[[package]]
-name = "importlib-metadata"
-version = "8.6.1"
-description = "Read metadata from Python packages"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
-    {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
-]
-
-[package.dependencies]
-zipp = ">=3.20"
-
-[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
-cover = ["pytest-cov"]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-enabler = ["pytest-enabler (>=2.2)"]
-perf = ["ipython"]
-test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
-type = ["pytest-mypy"]
-
-[[package]]
-name = "iniconfig"
-version = "2.0.0"
-description = "brain-dead simple config-ini parsing"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
-    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
-]
-
-[[package]]
-name = "jaraco-classes"
-version = "3.4.0"
-description = "Utility functions for Python class constructs"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"},
-    {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"},
-]
-
-[package.dependencies]
-more-itertools = "*"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
-
-[[package]]
-name = "jaraco-context"
-version = "6.0.1"
-description = "Useful decorators and context managers"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"},
-    {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"},
-]
-
-[package.dependencies]
-"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""}
-
-[package.extras]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
-
-[[package]]
-name = "jaraco-functools"
-version = "4.1.0"
-description = "Functools like those found in stdlib"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"},
-    {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"},
-]
-
-[package.dependencies]
-more-itertools = "*"
-
-[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
-cover = ["pytest-cov"]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-enabler = ["pytest-enabler (>=2.2)"]
-test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"]
-type = ["pytest-mypy"]
-
-[[package]]
-name = "jeepney"
-version = "0.8.0"
-description = "Low-level, pure Python DBus protocol wrapper."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"},
-    {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"},
-]
-
-[package.extras]
-test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
-trio = ["async_generator", "trio"]
-
-[[package]]
-name = "jinja2"
-version = "3.1.5"
-description = "A very fast and expressive template engine."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
-    {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
-]
-
-[package.dependencies]
-MarkupSafe = ">=2.0"
-
-[package.extras]
-i18n = ["Babel (>=2.7)"]
-
-[[package]]
-name = "keyring"
-version = "25.6.0"
-description = "Store and access your passwords safely."
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"},
-    {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"},
-]
-
-[package.dependencies]
-importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
-"jaraco.classes" = "*"
-"jaraco.context" = "*"
-"jaraco.functools" = "*"
-jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
-pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
-SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
-
-[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
-completion = ["shtab (>=1.1.0)"]
-cover = ["pytest-cov"]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-enabler = ["pytest-enabler (>=2.2)"]
-test = ["pyfakefs", "pytest (>=6,!=8.1.*)"]
-type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"]
-
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-description = "Python port of markdown-it. Markdown parsing, done right!"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
-    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
-]
-
-[package.dependencies]
-mdurl = ">=0.1,<1.0"
-
-[package.extras]
-benchmarking = ["psutil", "pytest", "pytest-benchmark"]
-code-style = ["pre-commit (>=3.0,<4.0)"]
-compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
-linkify = ["linkify-it-py (>=1,<3)"]
-plugins = ["mdit-py-plugins"]
-profiling = ["gprof2dot"]
-rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
-testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.2"
-description = "Safely add untrusted strings to HTML/XML markup."
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
-    {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
-    {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
-    {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
-    {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
-    {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
-    {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
-]
-
-[[package]]
-name = "mccabe"
-version = "0.7.0"
-description = "McCabe checker, plugin for flake8"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
-    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-description = "Markdown URL utilities"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
-    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
-]
-
-[[package]]
-name = "more-itertools"
-version = "10.6.0"
-description = "More routines for operating on iterables, beyond itertools"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"},
-    {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"},
-]
-
-[[package]]
-name = "nh3"
-version = "0.2.20"
-description = "Python binding to Ammonia HTML sanitizer Rust crate"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"},
-    {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"},
-    {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"},
-    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"},
-    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"},
-    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"},
-    {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"},
-    {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"},
-    {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"},
-    {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"},
-    {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"},
-    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"},
-    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"},
-    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"},
-    {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"},
-    {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"},
-    {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"},
-    {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"},
-]
-
-[[package]]
-name = "packaging"
-version = "24.2"
-description = "Core utilities for Python packages"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
-    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
-]
-
-[[package]]
-name = "pluggy"
-version = "1.5.0"
-description = "plugin and hook calling mechanisms for python"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
-    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
-]
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
-
-[[package]]
-name = "pycodestyle"
-version = "2.12.1"
-description = "Python style guide checker"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
-    {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
-]
-
-[[package]]
-name = "pycparser"
-version = "2.22"
-description = "C parser in Python"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
-    {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
-]
-
-[[package]]
-name = "pyflakes"
-version = "3.2.0"
-description = "passive checker of Python programs"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
-    {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.1"
-description = "Pygments is a syntax highlighting package written in Python."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
-    {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
-]
-
-[package.extras]
-windows-terminal = ["colorama (>=0.4.6)"]
-
-[[package]]
-name = "pytest"
-version = "8.3.4"
-description = "pytest: simple powerful testing with Python"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
-    {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
-]
-
-[package.dependencies]
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
-iniconfig = "*"
-packaging = "*"
-pluggy = ">=1.5,<2"
-tomli = {version = ">=1", markers = "python_version < \"3.11\""}
-
-[package.extras]
-dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
-
-[[package]]
-name = "pytest-cov"
-version = "6.0.0"
-description = "Pytest plugin for measuring coverage."
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
-    {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
-]
-
-[package.dependencies]
-coverage = {version = ">=7.5", extras = ["toml"]}
-pytest = ">=4.6"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
-
-[[package]]
-name = "pywin32-ctypes"
-version = "0.2.3"
-description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
-    {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
-]
-
-[[package]]
-name = "readme-renderer"
-version = "44.0"
-description = "readme_renderer is a library for rendering readme descriptions for Warehouse"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"},
-    {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"},
-]
-
-[package.dependencies]
-docutils = ">=0.21.2"
-nh3 = ">=0.2.14"
-Pygments = ">=2.5.1"
-
-[package.extras]
-md = ["cmarkgfm (>=0.8.0)"]
-
-[[package]]
-name = "requests"
-version = "2.32.3"
-description = "Python HTTP for Humans."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
-    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
-]
-
-[package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
-idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
-
-[package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
-
-[[package]]
-name = "requests-toolbelt"
-version = "1.0.0"
-description = "A utility belt for advanced users of python-requests"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
-    {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
-    {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
-]
-
-[package.dependencies]
-requests = ">=2.0.1,<3.0.0"
-
-[[package]]
-name = "rfc3986"
-version = "2.0.0"
-description = "Validating URI References per RFC 3986"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"},
-    {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"},
-]
-
-[package.extras]
-idna2008 = ["idna"]
-
-[[package]]
-name = "rich"
-version = "13.9.4"
-description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-optional = false
-python-versions = ">=3.8.0"
-files = [
-    {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
-    {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
-]
-
-[package.dependencies]
-markdown-it-py = ">=2.2.0"
-pygments = ">=2.13.0,<3.0.0"
-typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
-
-[package.extras]
-jupyter = ["ipywidgets (>=7.5.1,<9)"]
-
-[[package]]
-name = "secretstorage"
-version = "3.3.3"
-description = "Python bindings to FreeDesktop.org Secret Service API"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
-    {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
-]
-
-[package.dependencies]
-cryptography = ">=2.0"
-jeepney = ">=0.6"
-
-[[package]]
-name = "snowballstemmer"
-version = "2.2.0"
-description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-optional = false
-python-versions = "*"
-files = [
-    {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
-    {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
-]
-
-[[package]]
-name = "soupsieve"
-version = "2.6"
-description = "A modern CSS selector implementation for Beautiful Soup."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
-    {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
-]
-
-[[package]]
-name = "sphinx"
-version = "8.1.3"
-description = "Python documentation generator"
-optional = false
-python-versions = ">=3.10"
-files = [
-    {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
-    {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
-]
-
-[package.dependencies]
-alabaster = ">=0.7.14"
-babel = ">=2.13"
-colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
-docutils = ">=0.20,<0.22"
-imagesize = ">=1.3"
-Jinja2 = ">=3.1"
-packaging = ">=23.0"
-Pygments = ">=2.17"
-requests = ">=2.30.0"
-snowballstemmer = ">=2.2"
-sphinxcontrib-applehelp = ">=1.0.7"
-sphinxcontrib-devhelp = ">=1.0.6"
-sphinxcontrib-htmlhelp = ">=2.0.6"
-sphinxcontrib-jsmath = ">=1.0.1"
-sphinxcontrib-qthelp = ">=1.0.6"
-sphinxcontrib-serializinghtml = ">=1.1.9"
-tomli = {version = ">=2", markers = "python_version < \"3.11\""}
-
-[package.extras]
-docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
-test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
-
-[[package]]
-name = "sphinx-basic-ng"
-version = "1.0.0b2"
-description = "A modern skeleton for Sphinx themes."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"},
-    {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"},
-]
-
-[package.dependencies]
-sphinx = ">=4.0"
-
-[package.extras]
-docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"]
-
-[[package]]
-name = "sphinxcontrib-applehelp"
-version = "2.0.0"
-description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"},
-    {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-devhelp"
-version = "2.0.0"
-description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"},
-    {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-htmlhelp"
-version = "2.1.0"
-description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"},
-    {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["html5lib", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-jsmath"
-version = "1.0.1"
-description = "A sphinx extension which renders display math in HTML via JavaScript"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
-    {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
-]
-
-[package.extras]
-test = ["flake8", "mypy", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-qthelp"
-version = "2.0.0"
-description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"},
-    {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["defusedxml (>=0.7.1)", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-serializinghtml"
-version = "2.0.0"
-description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"},
-    {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
-[[package]]
-name = "tomli"
-version = "2.2.1"
-description = "A lil' TOML parser"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
-    {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
-    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
-    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
-    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
-    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
-    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
-    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
-    {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
-    {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
-    {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
-    {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
-    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
-    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
-    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
-    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
-    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
-    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
-    {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
-    {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
-    {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
-    {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
-    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
-    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
-    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
-    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
-    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
-    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
-    {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
-    {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
-    {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
-    {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
-]
-
-[[package]]
-name = "twine"
-version = "6.1.0"
-description = "Collection of utilities for publishing packages on PyPI"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"},
-    {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"},
-]
-
-[package.dependencies]
-id = "*"
-importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
-keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""}
-packaging = ">=24.0"
-readme-renderer = ">=35.0"
-requests = ">=2.20"
-requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
-rfc3986 = ">=1.4.0"
-rich = ">=12.0.0"
-urllib3 = ">=1.26.0"
-
-[package.extras]
-keyring = ["keyring (>=15.1)"]
-
-[[package]]
-name = "typing-extensions"
-version = "4.12.2"
-description = "Backported and Experimental Type Hints for Python 3.8+"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
-    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
-]
-
-[[package]]
-name = "urllib3"
-version = "2.3.0"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
-    {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
-]
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-h2 = ["h2 (>=4,<5)"]
-socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
-
-[[package]]
-name = "zipp"
-version = "3.21.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
-    {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
-]
-
-[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
-cover = ["pytest-cov"]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-enabler = ["pytest-enabler (>=2.2)"]
-test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
-type = ["pytest-mypy"]
-
-[metadata]
-lock-version = "2.0"
-python-versions = "^3.9"
-content-hash = "b1451f3a657a7a8be43a5f48a3c47af4b56ebf312cc19da9e2230af3973f8fc3"
diff --git a/pyproject.toml b/pyproject.toml
index c69536be7..00cb5eef7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,13 @@
-[tool.poetry]
+[project]
+authors = [
+    {name = "Brian May", email = "brian@linuxpenguins.xyz"},
+]
+license = {text = "LGPL-2.1"}
+requires-python = "<4.0,>=3.9"
+dependencies = []
 name = "sshuttle"
 version = "1.3.0"
 description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
-authors = ["Brian May "]
-license = "LGPL-2.1"
 readme = "README.rst"
 classifiers = [
     "Development Status :: 5 - Production/Stable",
@@ -18,27 +22,31 @@ classifiers = [
     "Topic :: System :: Networking",
 ]
 
-[tool.poetry.dependencies]
-python = "^3.9"
-
-[tool.poetry.group.dev.dependencies]
-pytest = "^8.0.1"
-pytest-cov = ">=4.1,<7.0"
-flake8 = "^7.0.0"
-pyflakes = "^3.2.0"
-bump2version = "^1.0.1"
-twine = ">=5,<7"
+[project.scripts]
+sshuttle = "sshuttle.cmdline:main"
 
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
+[dependency-groups]
+dev = [
+    "pytest<9.0.0,>=8.0.1",
+    "pytest-cov<7.0,>=4.1",
+    "flake8<8.0.0,>=7.0.0",
+    "pyflakes<4.0.0,>=3.2.0",
+    "bump2version<2.0.0,>=1.0.1",
+    "twine<7,>=5",
+]
+docs = [
+    "sphinx==8.1.3; python_version ~= \"3.10\"",
+    "furo==2024.8.6",
+]
 
-[tool.poetry.scripts]
-sshuttle = "sshuttle.cmdline:main"
+[tool.uv]
+default-groups = []
 
-[tool.poetry.group.docs]
-optional = true
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
 
-[tool.poetry.group.docs.dependencies]
-sphinx = { version = "8.1.3", python = ">=3.10,<4.0" }
-furo = "2024.8.6"
+[tool.hatch.build.targets.sdist]
+exclude = [
+    "/.jj"
+]
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 000000000..0d0c01a6a
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1061 @@
+version = 1
+revision = 1
+requires-python = ">=3.9, <4.0"
+resolution-markers = [
+    "python_full_version < '3.10'",
+    "python_full_version >= '3.10'",
+]
+
+[[package]]
+name = "alabaster"
+version = "0.7.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 },
+]
+
+[[package]]
+name = "babel"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 },
+]
+
+[[package]]
+name = "backports-tarfile"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "soupsieve" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
+]
+
+[[package]]
+name = "bump2version"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
+    { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
+    { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
+    { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
+    { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
+    { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
+    { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
+    { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
+    { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
+    { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
+    { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
+    { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
+    { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
+    { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
+    { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
+    { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
+    { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+    { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+    { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+    { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+    { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+    { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+    { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+    { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+    { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+    { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+    { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+    { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+    { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+    { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+    { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 },
+    { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 },
+    { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 },
+    { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 },
+    { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 },
+    { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 },
+    { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 },
+    { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
+    { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
+    { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
+    { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
+    { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
+    { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
+    { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
+    { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
+    { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
+    { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
+    { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
+    { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
+    { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
+    { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
+    { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
+    { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
+    { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
+    { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
+    { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
+    { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
+    { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
+    { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
+    { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
+    { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
+    { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
+    { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
+    { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+    { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+    { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+    { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+    { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+    { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+    { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+    { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+    { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+    { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+    { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+    { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+    { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+    { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+    { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+    { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+    { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+    { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+    { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+    { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+    { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+    { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+    { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+    { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+    { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+    { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+    { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 },
+    { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 },
+    { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 },
+    { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 },
+    { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 },
+    { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 },
+    { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 },
+    { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 },
+    { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 },
+    { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 },
+    { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 },
+    { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 },
+    { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 },
+    { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "coverage"
+version = "7.6.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 },
+    { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 },
+    { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 },
+    { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 },
+    { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 },
+    { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 },
+    { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 },
+    { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 },
+    { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 },
+    { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 },
+    { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 },
+    { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 },
+    { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 },
+    { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 },
+    { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 },
+    { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 },
+    { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 },
+    { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 },
+    { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 },
+    { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 },
+    { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
+    { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
+    { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
+    { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
+    { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
+    { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
+    { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
+    { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
+    { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
+    { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
+    { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 },
+    { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 },
+    { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 },
+    { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 },
+    { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 },
+    { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 },
+    { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 },
+    { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 },
+    { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 },
+    { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 },
+    { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 },
+    { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 },
+    { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 },
+    { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 },
+    { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 },
+    { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 },
+    { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 },
+    { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 },
+    { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 },
+    { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 },
+    { url = "https://files.pythonhosted.org/packages/6c/eb/cf062b1c3dbdcafd64a2a154beea2e4aa8e9886c34e41f53fa04925c8b35/coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d", size = 208343 },
+    { url = "https://files.pythonhosted.org/packages/95/42/4ebad0ab065228e29869a060644712ab1b0821d8c29bfefa20c2118c9e19/coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929", size = 208769 },
+    { url = "https://files.pythonhosted.org/packages/44/9f/421e84f7f9455eca85ff85546f26cbc144034bb2587e08bfc214dd6e9c8f/coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87", size = 237553 },
+    { url = "https://files.pythonhosted.org/packages/c9/c4/a2c4f274bcb711ed5db2ccc1b851ca1c45f35ed6077aec9d6c61845d80e3/coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c", size = 235473 },
+    { url = "https://files.pythonhosted.org/packages/e0/10/a3d317e38e5627b06debe861d6c511b1611dd9dc0e2a47afbe6257ffd341/coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2", size = 236575 },
+    { url = "https://files.pythonhosted.org/packages/4d/49/51cd991b56257d2e07e3d5cb053411e9de5b0f4e98047167ec05e4e19b55/coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd", size = 235690 },
+    { url = "https://files.pythonhosted.org/packages/f7/87/631e5883fe0a80683a1f20dadbd0f99b79e17a9d8ea9aff3a9b4cfe50b93/coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73", size = 234040 },
+    { url = "https://files.pythonhosted.org/packages/7c/34/edd03f6933f766ec97dddd178a7295855f8207bb708dbac03777107ace5b/coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86", size = 235048 },
+    { url = "https://files.pythonhosted.org/packages/ee/1e/d45045b7d3012fe518c617a57b9f9396cdaebe6455f1b404858b32c38cdd/coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31", size = 211085 },
+    { url = "https://files.pythonhosted.org/packages/df/ea/086cb06af14a84fe773b86aa140892006a906c5ec947e609ceb6a93f6257/coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57", size = 211965 },
+    { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 },
+    { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
+]
+
+[package.optional-dependencies]
+toml = [
+    { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "44.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
+    { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
+    { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
+    { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
+    { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
+    { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
+    { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
+    { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
+    { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
+    { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
+    { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
+    { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
+    { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
+    { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
+    { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
+    { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
+    { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
+    { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
+    { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 },
+    { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 },
+    { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 },
+    { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 },
+    { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 },
+    { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 },
+    { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 },
+    { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 },
+]
+
+[[package]]
+name = "docutils"
+version = "0.21.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "flake8"
+version = "7.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mccabe" },
+    { name = "pycodestyle" },
+    { name = "pyflakes" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745 },
+]
+
+[[package]]
+name = "furo"
+version = "2024.8.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "beautifulsoup4" },
+    { name = "pygments" },
+    { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+    { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+    { name = "sphinx-basic-ng" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 },
+]
+
+[[package]]
+name = "id"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+]
+
+[[package]]
+name = "jaraco-classes"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 },
+]
+
+[[package]]
+name = "jaraco-context"
+version = "6.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "backports-tarfile", marker = "python_full_version < '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 },
+]
+
+[[package]]
+name = "jaraco-functools"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 },
+]
+
+[[package]]
+name = "jeepney"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
+]
+
+[[package]]
+name = "keyring"
+version = "25.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "importlib-metadata", marker = "python_full_version < '3.12'" },
+    { name = "jaraco-classes" },
+    { name = "jaraco-context" },
+    { name = "jaraco-functools" },
+    { name = "jeepney", marker = "sys_platform == 'linux'" },
+    { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
+    { name = "secretstorage", marker = "sys_platform == 'linux'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 },
+    { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 },
+    { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 },
+    { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 },
+    { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 },
+    { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 },
+    { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 },
+    { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 },
+    { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 },
+    { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 },
+    { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
+    { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
+    { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
+    { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
+    { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
+    { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
+    { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
+    { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
+    { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
+    { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
+    { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+    { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+    { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+    { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+    { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+    { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+    { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+    { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+    { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+    { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+    { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+    { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+    { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+    { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+    { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+    { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+    { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+    { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+    { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+    { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+    { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+    { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+    { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+    { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+    { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+    { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+    { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+    { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+    { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+    { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+    { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 },
+    { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 },
+    { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 },
+    { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 },
+    { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 },
+    { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 },
+    { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 },
+    { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 },
+    { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 },
+    { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 },
+]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 },
+]
+
+[[package]]
+name = "nh3"
+version = "0.2.21"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678 },
+    { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774 },
+    { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012 },
+    { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619 },
+    { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384 },
+    { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908 },
+    { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180 },
+    { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747 },
+    { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908 },
+    { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 },
+    { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 },
+    { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 },
+    { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 },
+    { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 },
+    { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 },
+    { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 },
+    { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 },
+    { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 },
+    { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 },
+    { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 },
+    { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 },
+    { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 },
+    { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
+[[package]]
+name = "pycodestyle"
+version = "2.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
+[[package]]
+name = "pyflakes"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "coverage", extra = ["toml"] },
+    { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
+]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
+]
+
+[[package]]
+name = "readme-renderer"
+version = "44.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "docutils" },
+    { name = "nh3" },
+    { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "charset-normalizer" },
+    { name = "idna" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 },
+]
+
+[[package]]
+name = "rfc3986"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 },
+]
+
+[[package]]
+name = "rich"
+version = "13.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py" },
+    { name = "pygments" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
+]
+
+[[package]]
+name = "secretstorage"
+version = "3.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cryptography" },
+    { name = "jeepney" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 },
+]
+
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
+]
+
+[[package]]
+name = "sphinx"
+version = "7.4.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+    "python_full_version < '3.10'",
+]
+dependencies = [
+    { name = "alabaster", marker = "python_full_version < '3.10'" },
+    { name = "babel", marker = "python_full_version < '3.10'" },
+    { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+    { name = "docutils", marker = "python_full_version < '3.10'" },
+    { name = "imagesize", marker = "python_full_version < '3.10'" },
+    { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+    { name = "jinja2", marker = "python_full_version < '3.10'" },
+    { name = "packaging", marker = "python_full_version < '3.10'" },
+    { name = "pygments", marker = "python_full_version < '3.10'" },
+    { name = "requests", marker = "python_full_version < '3.10'" },
+    { name = "snowballstemmer", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" },
+    { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" },
+    { name = "tomli", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 },
+]
+
+[[package]]
+name = "sphinx"
+version = "8.1.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+    "python_full_version >= '3.10'",
+]
+dependencies = [
+    { name = "alabaster", marker = "python_full_version >= '3.10'" },
+    { name = "babel", marker = "python_full_version >= '3.10'" },
+    { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+    { name = "docutils", marker = "python_full_version >= '3.10'" },
+    { name = "imagesize", marker = "python_full_version >= '3.10'" },
+    { name = "jinja2", marker = "python_full_version >= '3.10'" },
+    { name = "packaging", marker = "python_full_version >= '3.10'" },
+    { name = "pygments", marker = "python_full_version >= '3.10'" },
+    { name = "requests", marker = "python_full_version >= '3.10'" },
+    { name = "snowballstemmer", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.10'" },
+    { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.10'" },
+    { name = "tomli", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 },
+]
+
+[[package]]
+name = "sphinx-basic-ng"
+version = "1.0.0b2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+    { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 },
+]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 },
+]
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 },
+]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 },
+]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 },
+]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 },
+]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 },
+]
+
+[[package]]
+name = "sshuttle"
+version = "1.3.0"
+source = { editable = "." }
+
+[package.dev-dependencies]
+dev = [
+    { name = "bump2version" },
+    { name = "flake8" },
+    { name = "pyflakes" },
+    { name = "pytest" },
+    { name = "pytest-cov" },
+    { name = "twine" },
+]
+docs = [
+    { name = "furo" },
+    { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [
+    { name = "bump2version", specifier = ">=1.0.1,<2.0.0" },
+    { name = "flake8", specifier = ">=7.0.0,<8.0.0" },
+    { name = "pyflakes", specifier = ">=3.2.0,<4.0.0" },
+    { name = "pytest", specifier = ">=8.0.1,<9.0.0" },
+    { name = "pytest-cov", specifier = ">=4.1,<7.0" },
+    { name = "twine", specifier = ">=5,<7" },
+]
+docs = [
+    { name = "furo", specifier = "==2024.8.6" },
+    { name = "sphinx", marker = "python_full_version >= '3.10' and python_full_version < '4'", specifier = "==8.1.3" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+    { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+    { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+    { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+    { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+    { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+    { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+    { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+    { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+    { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+    { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+    { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+    { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+    { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+    { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+    { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+    { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+    { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+    { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+    { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+    { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+    { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+    { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+    { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+    { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+    { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+    { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+    { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+    { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+    { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+    { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+]
+
+[[package]]
+name = "twine"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "id" },
+    { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+    { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
+    { name = "packaging" },
+    { name = "readme-renderer" },
+    { name = "requests" },
+    { name = "requests-toolbelt" },
+    { name = "rfc3986" },
+    { name = "rich" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
+[[package]]
+name = "zipp"
+version = "3.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 },
+]

From 684417d36376b0f1aeee87e77f130da124e83728 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Sun, 2 Mar 2025 19:03:53 +1100
Subject: [PATCH 265/275] build: convert from poetry to uv


From 454262829ccdcd6e8e309d841505579562a1b5e8 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Mon, 3 Mar 2025 08:58:26 +1100
Subject: [PATCH 266/275] ci: attempt to use dependabot beta support for uv

See https://github.com/dependabot/dependabot-core/issues/10478#issuecomment-2691330949
---
 .github/dependabot.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4a15f2acb..cfb15feaa 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,6 @@
 version: 2
 updates:
-- package-ecosystem: pip
+- package-ecosystem: uv
   directory: "/"
   schedule:
     interval: daily

From cf867248c2541b3d5c84c3d550219632506b2eef Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 4 Mar 2025 07:47:09 +1100
Subject: [PATCH 267/275] ci: attempt to use dependabot beta support for uv (2)

I can read the instructions. Really!
---
 .github/dependabot.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index cfb15feaa..d1093657e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,4 +1,5 @@
 version: 2
+enable-beta-ecosystems: true
 updates:
 - package-ecosystem: uv
   directory: "/"

From 7b662536ba92d724ed8f86a32a21282fea66047c Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Wed, 12 Mar 2025 08:45:02 +1100
Subject: [PATCH 268/275] fix: correct bad version number at runtime

---
 sshuttle/__init__.py | 5 +----
 sshuttle/version.py  | 1 -
 2 files changed, 1 insertion(+), 5 deletions(-)
 delete mode 100644 sshuttle/version.py

diff --git a/sshuttle/__init__.py b/sshuttle/__init__.py
index a6ab7f4c8..67bc602ab 100644
--- a/sshuttle/__init__.py
+++ b/sshuttle/__init__.py
@@ -1,4 +1 @@
-try:
-    from sshuttle.version import version as __version__
-except ImportError:
-    __version__ = "unknown"
+__version__ = "1.3.0"
diff --git a/sshuttle/version.py b/sshuttle/version.py
deleted file mode 100644
index af8fb55e1..000000000
--- a/sshuttle/version.py
+++ /dev/null
@@ -1 +0,0 @@
-__version__ = version = '1.2.0'

From 63f94aa6ec6514e5503be90088e3fa55275fb4e6 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Wed, 12 Mar 2025 08:52:55 +1100
Subject: [PATCH 269/275] build: fix readthedocs build version number

---
 docs/conf.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/conf.py b/docs/conf.py
index c69f00197..0d33ba506 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,7 +16,7 @@
 import sys
 import os
 sys.path.insert(0, os.path.abspath('..'))
-import sshuttle.version  # NOQA
+import sshuttle  # NOQA
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -56,7 +56,7 @@
 # built documents.
 #
 # The full version, including alpha/beta/rc tags.
-release = sshuttle.version.version
+release = sshuttle.__version__
 # The short X.Y version.
 version = '.'.join(release.split('.')[:2])
 

From ae3c022d1d67de92f1c4712d06eb8ae76c970624 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 25 Mar 2025 12:03:38 +1100
Subject: [PATCH 270/275] fix: add python lint tools

---
 flake.nix      |   3 +
 pyproject.toml |   5 +
 uv.lock        | 366 ++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 373 insertions(+), 1 deletion(-)

diff --git a/flake.nix b/flake.nix
index 2bdf72340..615f1852e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -71,6 +71,9 @@
               chardet.setuptools = [ ];
               colorlog.setuptools = [ ];
               python-debian.setuptools = [ ];
+              pluggy.setuptools = [ ];
+              pathspec.flit-core = [ ];
+              packaging.flit-core = [ ];
             };
 
           in
diff --git a/pyproject.toml b/pyproject.toml
index 00cb5eef7..2a4f78987 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,11 @@ dev = [
     "pyflakes<4.0.0,>=3.2.0",
     "bump2version<2.0.0,>=1.0.1",
     "twine<7,>=5",
+    "black>=25.1.0",
+    "jedi-language-server>=0.44.0",
+    "pylsp-mypy>=0.7.0",
+    "python-lsp-server>=1.12.2",
+    "ruff>=0.11.2",
 ]
 docs = [
     "sphinx==8.1.3; python_version ~= \"3.10\"",
diff --git a/uv.lock b/uv.lock
index 0d0c01a6a..38eca2c42 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,4 @@
 version = 1
-revision = 1
 requires-python = ">=3.9, <4.0"
 resolution-markers = [
     "python_full_version < '3.10'",
@@ -15,6 +14,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 },
 ]
 
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+]
+
 [[package]]
 name = "babel"
 version = "2.17.0"
@@ -46,6 +54,44 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
 ]
 
+[[package]]
+name = "black"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "mypy-extensions" },
+    { name = "packaging" },
+    { name = "pathspec" },
+    { name = "platformdirs" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 },
+    { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 },
+    { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 },
+    { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 },
+    { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 },
+    { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 },
+    { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 },
+    { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 },
+    { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
+    { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
+    { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
+    { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
+    { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
+    { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
+    { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
+    { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
+    { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593 },
+    { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000 },
+    { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963 },
+    { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419 },
+    { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
+]
+
 [[package]]
 name = "bump2version"
 version = "1.0.1"
@@ -55,6 +101,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030 },
 ]
 
+[[package]]
+name = "cattrs"
+version = "24.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/65/af6d57da2cb32c076319b7489ae0958f746949d407109e3ccf4d115f147c/cattrs-24.1.2.tar.gz", hash = "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85", size = 426462 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c8/d5/867e75361fc45f6de75fe277dd085627a9db5ebb511a87f27dc1396b5351/cattrs-24.1.2-py3-none-any.whl", hash = "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0", size = 66446 },
+]
+
 [[package]]
 name = "certifi"
 version = "2025.1.31"
@@ -187,6 +247,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
 ]
 
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "platform_system == 'Windows'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -308,6 +380,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 },
 ]
 
+[[package]]
+name = "docstring-to-markdown"
+version = "0.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7a/ad/6a66abd14676619bd56f6b924c96321a2e2d7d86558841d94a30023eec53/docstring-to-markdown-0.15.tar.gz", hash = "sha256:e146114d9c50c181b1d25505054a8d0f7a476837f0da2c19f07e06eaed52b73d", size = 29246 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c1/cf/4eee59f6c4111b3e80cc32cf6bac483a90646f5c8693e84496c9855e8e38/docstring_to_markdown-0.15-py3-none-any.whl", hash = "sha256:27afb3faedba81e34c33521c32bbd258d7fbb79eedf7d29bc4e81080e854aec0", size = 21640 },
+]
+
 [[package]]
 name = "docutils"
 version = "0.21.2"
@@ -443,6 +524,35 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 },
 ]
 
+[[package]]
+name = "jedi"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 },
+]
+
+[[package]]
+name = "jedi-language-server"
+version = "0.44.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cattrs" },
+    { name = "docstring-to-markdown" },
+    { name = "jedi" },
+    { name = "lsprotocol" },
+    { name = "pygls" },
+    { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/ca/97ec6c7acc8e7718816283d3140ebd9601781d731be753c8d0eb97fca541/jedi_language_server-0.44.0.tar.gz", hash = "sha256:276536bd00e64e65753d54cd35237d62730daffd65292dc8510d3063ebaefe4d", size = 32564 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3b/04/52ec51105a38c6807162c9c48c006b754885574eb52a7ed0bf10369c1a30/jedi_language_server-0.44.0-py3-none-any.whl", hash = "sha256:17619fac5faf7111036c0b01d460c4eb848ce8df1af8208d85c255db34ec2eff", size = 31794 },
+]
+
 [[package]]
 name = "jeepney"
 version = "0.9.0"
@@ -482,6 +592,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 },
 ]
 
+[[package]]
+name = "lsprotocol"
+version = "2023.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "cattrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826 },
+]
+
 [[package]]
 name = "markdown-it-py"
 version = "3.0.0"
@@ -589,6 +712,59 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 },
 ]
 
+[[package]]
+name = "mypy"
+version = "1.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mypy-extensions" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 },
+    { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 },
+    { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 },
+    { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 },
+    { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 },
+    { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 },
+    { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
+    { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
+    { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
+    { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
+    { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
+    { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
+    { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
+    { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
+    { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
+    { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
+    { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
+    { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
+    { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
+    { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
+    { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
+    { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
+    { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
+    { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
+    { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129 },
+    { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335 },
+    { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935 },
+    { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827 },
+    { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924 },
+    { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176 },
+    { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
+]
+
 [[package]]
 name = "nh3"
 version = "0.2.21"
@@ -629,6 +805,33 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
 ]
 
+[[package]]
+name = "parso"
+version = "0.8.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
+]
+
 [[package]]
 name = "pluggy"
 version = "1.5.0"
@@ -665,6 +868,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 },
 ]
 
+[[package]]
+name = "pygls"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cattrs" },
+    { name = "lsprotocol" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031 },
+]
+
 [[package]]
 name = "pygments"
 version = "2.19.1"
@@ -674,6 +890,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
 ]
 
+[[package]]
+name = "pylsp-mypy"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mypy" },
+    { name = "python-lsp-server" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/4d/9683a57f2e8b9263910ef497a99d88622f4fb1c158decb867fd40a41bfdd/pylsp_mypy-0.7.0.tar.gz", hash = "sha256:e94f531d4ce523222c2af7471abe396cfeb4cc3c4b181d54462fb6d553e1e0b3", size = 18529 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a6/7d/324859fa4af565db32ff8d924fd10dd49922756736be12d783be3813ffc8/pylsp_mypy-0.7.0-py3-none-any.whl", hash = "sha256:756377d05d251d2e31d1963397654149b9c1ea5b0ba1aedd74adef76decd32e9", size = 12232 },
+]
+
 [[package]]
 name = "pytest"
 version = "8.3.4"
@@ -704,6 +934,35 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
 ]
 
+[[package]]
+name = "python-lsp-jsonrpc"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "ujson" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/b6/fd92e2ea4635d88966bb42c20198df1a981340f07843b5e3c6694ba3557b/python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912", size = 15298 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/d9/656659d5b5d5f402b2b174cd0ba9bc827e07ce3c0bf88da65424baf64af8/python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c", size = 8805 },
+]
+
+[[package]]
+name = "python-lsp-server"
+version = "1.12.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "docstring-to-markdown" },
+    { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+    { name = "jedi" },
+    { name = "pluggy" },
+    { name = "python-lsp-jsonrpc" },
+    { name = "ujson" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/0f/3d63c5f37edca529a2a003a30add97dcce67a83a99dd932528f623aa1df9/python_lsp_server-1.12.2.tar.gz", hash = "sha256:fea039a36b3132774d0f803671184cf7dde0c688e7b924f23a6359a66094126d", size = 115054 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/e7/28010a326ef591e1409daf9d57a47de94156c147ad1befe74d8196d82729/python_lsp_server-1.12.2-py3-none-any.whl", hash = "sha256:750116459449184ba20811167cdf96f91296ae12f1f65ebd975c5c159388111b", size = 74773 },
+]
+
 [[package]]
 name = "pywin32-ctypes"
 version = "0.2.3"
@@ -777,6 +1036,31 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
 ]
 
+[[package]]
+name = "ruff"
+version = "0.11.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
+    { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
+    { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
+    { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
+    { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
+    { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
+    { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
+    { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
+    { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
+    { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
+    { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
+    { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
+    { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
+    { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
+    { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
+    { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
+    { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
+]
+
 [[package]]
 name = "secretstorage"
 version = "3.3.3"
@@ -945,11 +1229,16 @@ source = { editable = "." }
 
 [package.dev-dependencies]
 dev = [
+    { name = "black" },
     { name = "bump2version" },
     { name = "flake8" },
+    { name = "jedi-language-server" },
     { name = "pyflakes" },
+    { name = "pylsp-mypy" },
     { name = "pytest" },
     { name = "pytest-cov" },
+    { name = "python-lsp-server" },
+    { name = "ruff" },
     { name = "twine" },
 ]
 docs = [
@@ -961,11 +1250,16 @@ docs = [
 
 [package.metadata.requires-dev]
 dev = [
+    { name = "black", specifier = ">=25.1.0" },
     { name = "bump2version", specifier = ">=1.0.1,<2.0.0" },
     { name = "flake8", specifier = ">=7.0.0,<8.0.0" },
+    { name = "jedi-language-server", specifier = ">=0.44.0" },
     { name = "pyflakes", specifier = ">=3.2.0,<4.0.0" },
+    { name = "pylsp-mypy", specifier = ">=0.7.0" },
     { name = "pytest", specifier = ">=8.0.1,<9.0.0" },
     { name = "pytest-cov", specifier = ">=4.1,<7.0" },
+    { name = "python-lsp-server", specifier = ">=1.12.2" },
+    { name = "ruff", specifier = ">=0.11.2" },
     { name = "twine", specifier = ">=5,<7" },
 ]
 docs = [
@@ -1042,6 +1336,76 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
 ]
 
+[[package]]
+name = "ujson"
+version = "5.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7d/91/91678e49a9194f527e60115db84368c237ac7824992224fac47dcb23a5c6/ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd", size = 55354 },
+    { url = "https://files.pythonhosted.org/packages/de/2f/1ed8c9b782fa4f44c26c1c4ec686d728a4865479da5712955daeef0b2e7b/ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf", size = 51808 },
+    { url = "https://files.pythonhosted.org/packages/51/bf/a3a38b2912288143e8e613c6c4c3f798b5e4e98c542deabf94c60237235f/ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6", size = 51995 },
+    { url = "https://files.pythonhosted.org/packages/b4/6d/0df8f7a6f1944ba619d93025ce468c9252aa10799d7140e07014dfc1a16c/ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569", size = 53566 },
+    { url = "https://files.pythonhosted.org/packages/d5/ec/370741e5e30d5f7dc7f31a478d5bec7537ce6bfb7f85e72acefbe09aa2b2/ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770", size = 58499 },
+    { url = "https://files.pythonhosted.org/packages/fe/29/72b33a88f7fae3c398f9ba3e74dc2e5875989b25f1c1f75489c048a2cf4e/ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1", size = 997881 },
+    { url = "https://files.pythonhosted.org/packages/70/5c/808fbf21470e7045d56a282cf5e85a0450eacdb347d871d4eb404270ee17/ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5", size = 1140631 },
+    { url = "https://files.pythonhosted.org/packages/8f/6a/e1e8281408e6270d6ecf2375af14d9e2f41c402ab6b161ecfa87a9727777/ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51", size = 1043511 },
+    { url = "https://files.pythonhosted.org/packages/cb/ca/e319acbe4863919ec62498bc1325309f5c14a3280318dca10fe1db3cb393/ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518", size = 38626 },
+    { url = "https://files.pythonhosted.org/packages/78/ec/dc96ca379de33f73b758d72e821ee4f129ccc32221f4eb3f089ff78d8370/ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f", size = 42076 },
+    { url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353 },
+    { url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813 },
+    { url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988 },
+    { url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561 },
+    { url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497 },
+    { url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877 },
+    { url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632 },
+    { url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513 },
+    { url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616 },
+    { url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071 },
+    { url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 },
+    { url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 },
+    { url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 },
+    { url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 },
+    { url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 },
+    { url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 },
+    { url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 },
+    { url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 },
+    { url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 },
+    { url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 },
+    { url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 },
+    { url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 },
+    { url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 },
+    { url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 },
+    { url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 },
+    { url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 },
+    { url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 },
+    { url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 },
+    { url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 },
+    { url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 },
+    { url = "https://files.pythonhosted.org/packages/97/94/50ff2f1b61d668907f20216873640ab19e0eaa77b51e64ee893f6adfb266/ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b", size = 55421 },
+    { url = "https://files.pythonhosted.org/packages/0c/b3/3d2ca621d8dbeaf6c5afd0725e1b4bbd465077acc69eff1e9302735d1432/ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27", size = 51816 },
+    { url = "https://files.pythonhosted.org/packages/8d/af/5dc103cb4d08f051f82d162a738adb9da488d1e3fafb9fd9290ea3eabf8e/ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76", size = 52023 },
+    { url = "https://files.pythonhosted.org/packages/5d/dd/b9a6027ba782b0072bf24a70929e15a58686668c32a37aebfcfaa9e00bdd/ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5", size = 53622 },
+    { url = "https://files.pythonhosted.org/packages/1f/28/bcf6df25c1a9f1989dc2ddc4ac8a80e246857e089f91a9079fd8a0a01459/ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0", size = 58563 },
+    { url = "https://files.pythonhosted.org/packages/9e/82/89404453a102d06d0937f6807c0a7ef2eec68b200b4ce4386127f3c28156/ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1", size = 998050 },
+    { url = "https://files.pythonhosted.org/packages/63/eb/2a4ea07165cad217bc842bb684b053bafa8ffdb818c47911c621e97a33fc/ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1", size = 1140672 },
+    { url = "https://files.pythonhosted.org/packages/72/53/d7bdf6afabeba3ed899f89d993c7f202481fa291d8c5be031c98a181eda4/ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996", size = 1043577 },
+    { url = "https://files.pythonhosted.org/packages/19/b1/75f5f0d18501fd34487e46829de3070724c7b350f1983ba7f07e0986720b/ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9", size = 38654 },
+    { url = "https://files.pythonhosted.org/packages/77/0d/50d2f9238f6d6683ead5ecd32d83d53f093a3c0047ae4c720b6d586cb80d/ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a", size = 42134 },
+    { url = "https://files.pythonhosted.org/packages/95/53/e5f5e733fc3525e65f36f533b0dbece5e5e2730b760e9beacf7e3d9d8b26/ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64", size = 51846 },
+    { url = "https://files.pythonhosted.org/packages/59/1f/f7bc02a54ea7b47f3dc2d125a106408f18b0f47b14fc737f0913483ae82b/ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3", size = 48103 },
+    { url = "https://files.pythonhosted.org/packages/1a/3a/d3921b6f29bc744d8d6c56db5f8bbcbe55115fd0f2b79c3c43ff292cc7c9/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a", size = 47257 },
+    { url = "https://files.pythonhosted.org/packages/f1/04/f4e3883204b786717038064afd537389ba7d31a72b437c1372297cb651ea/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746", size = 48468 },
+    { url = "https://files.pythonhosted.org/packages/17/cd/9c6547169eb01a22b04cbb638804ccaeb3c2ec2afc12303464e0f9b2ee5a/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88", size = 54266 },
+    { url = "https://files.pythonhosted.org/packages/70/bf/ecd14d3cf6127f8a990b01f0ad20e257f5619a555f47d707c57d39934894/ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b", size = 42224 },
+    { url = "https://files.pythonhosted.org/packages/8d/96/a3a2356ca5a4b67fe32a0c31e49226114d5154ba2464bb1220a93eb383e8/ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4", size = 51855 },
+    { url = "https://files.pythonhosted.org/packages/73/3d/41e78e7500e75eb6b5a7ab06907a6df35603b92ac6f939b86f40e9fe2c06/ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8", size = 48059 },
+    { url = "https://files.pythonhosted.org/packages/be/14/e435cbe5b5189483adbba5fe328e88418ccd54b2b1f74baa4172384bb5cd/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b", size = 47238 },
+    { url = "https://files.pythonhosted.org/packages/e8/d9/b6f4d1e6bec20a3b582b48f64eaa25209fd70dc2892b21656b273bc23434/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804", size = 48457 },
+    { url = "https://files.pythonhosted.org/packages/23/1c/cfefabb5996e21a1a4348852df7eb7cfc69299143739e86e5b1071c78735/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e", size = 54238 },
+    { url = "https://files.pythonhosted.org/packages/af/c4/fa70e77e1c27bbaf682d790bd09ef40e86807ada704c528ef3ea3418d439/ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7", size = 42230 },
+]
+
 [[package]]
 name = "urllib3"
 version = "2.3.0"

From 5942376090395d0a8dfe38fe012a519268199341 Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Tue, 25 Mar 2025 12:34:40 +1100
Subject: [PATCH 271/275] fix: add pycodestyle config

---
 setup.cfg | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/setup.cfg b/setup.cfg
index 69d69e405..781aae656 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -23,5 +23,8 @@ show-source = true
 statistics = true
 max-line-length = 128
 
+[pycodestyle]
+max-line-length = 128
+
 [tool:pytest]
 addopts = --cov=sshuttle --cov-branch --cov-report=term-missing

From 375810a9a8910a51db22c9fe4c0658c39b16c9e7 Mon Sep 17 00:00:00 2001
From: satarsa <727578+satarsa@users.noreply.github.com>
Date: Tue, 25 Mar 2025 21:26:38 +0100
Subject: [PATCH 272/275] fix: Restore "nft" method

Accidentally removed in refactoring (commit 900acc3).

Closes #1037.

Co-authored-by: Vadim Dyadkin 
---
 sshuttle/options.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshuttle/options.py b/sshuttle/options.py
index ca4993340..433abdf64 100644
--- a/sshuttle/options.py
+++ b/sshuttle/options.py
@@ -247,7 +247,7 @@ def convert_arg_line_to_args(self, arg_line):
 if sys.platform == 'win32':
     method_choices = ["auto", "windivert"]
 else:
-    method_choices = ["auto", "nat", "tproxy", "pf", "ipfw"]
+    method_choices = ["auto", "nft", "nat", "tproxy", "pf", "ipfw"]
 
 parser.add_argument(
     "--method",

From e2624f533f1dd1da67cede0db365e9d6efb2c26a Mon Sep 17 00:00:00 2001
From: Brian May 
Date: Wed, 26 Mar 2025 07:27:11 +1100
Subject: [PATCH 273/275] chore(master): release 1.3.1

---
 CHANGELOG.md         | 10 ++++++++++
 pyproject.toml       |  2 +-
 setup.cfg            |  2 +-
 sshuttle/__init__.py |  2 +-
 4 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6f3951c6..8fbc218a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [1.3.1](https://github.com/sshuttle/sshuttle/compare/v1.3.0...v1.3.1) (2025-03-25)
+
+
+### Bug Fixes
+
+* add pycodestyle config ([5942376](https://github.com/sshuttle/sshuttle/commit/5942376090395d0a8dfe38fe012a519268199341))
+* add python lint tools ([ae3c022](https://github.com/sshuttle/sshuttle/commit/ae3c022d1d67de92f1c4712d06eb8ae76c970624))
+* correct bad version number at runtime ([7b66253](https://github.com/sshuttle/sshuttle/commit/7b662536ba92d724ed8f86a32a21282fea66047c))
+* Restore "nft" method ([375810a](https://github.com/sshuttle/sshuttle/commit/375810a9a8910a51db22c9fe4c0658c39b16c9e7))
+
 ## [1.3.0](https://github.com/sshuttle/sshuttle/compare/v1.2.0...v1.3.0) (2025-02-23)
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 2a4f78987..c06f1ff5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ license = {text = "LGPL-2.1"}
 requires-python = "<4.0,>=3.9"
 dependencies = []
 name = "sshuttle"
-version = "1.3.0"
+version = "1.3.1"
 description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
 readme = "README.rst"
 classifiers = [
diff --git a/setup.cfg b/setup.cfg
index 781aae656..d4b09f3f3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 1.3.0
+current_version = 1.3.1
 
 [bumpversion:file:setup.py]
 
diff --git a/sshuttle/__init__.py b/sshuttle/__init__.py
index 67bc602ab..9c73af26b 100644
--- a/sshuttle/__init__.py
+++ b/sshuttle/__init__.py
@@ -1 +1 @@
-__version__ = "1.3.0"
+__version__ = "1.3.1"

From 934fac9d6c0f86223e3e7120148d346d9b20c9d0 Mon Sep 17 00:00:00 2001
From: Vighnesh Pathrikar <59459766+VighneshPath@users.noreply.github.com>
Date: Sat, 5 Apr 2025 02:17:57 +0530
Subject: [PATCH 274/275] fix: Updates sudoers config according to executable

- Sudoers config has not been working since the firewall command was updated in 32fceefa.
- This is to update the command for sudoers to keep it similar to what the client executes.
---
 sshuttle/sudoers.py | 29 ++++++++++++++---------------
 1 file changed, 14 insertions(+), 15 deletions(-)

diff --git a/sshuttle/sudoers.py b/sshuttle/sudoers.py
index 52874d282..d1c8b1e44 100644
--- a/sshuttle/sudoers.py
+++ b/sshuttle/sudoers.py
@@ -5,7 +5,15 @@
 
 
 def build_config(user_name):
-    template = '''
+    """Generates a sudoers configuration to allow passwordless execution of sshuttle."""
+
+    argv0 = os.path.abspath(sys.argv[0])
+    is_python_script = argv0.endswith('.py')
+    executable = f"{sys.executable} {argv0}" if is_python_script else argv0
+    dist_packages = os.path.dirname(os.path.abspath(__file__))
+    cmd_alias = f"SSHUTTLE{uuid4().hex[-3:].upper()}"
+
+    template = f"""
 # WARNING: If you intend to restrict a user to only running the
 # sshuttle command as root, THIS CONFIGURATION IS INSECURE.
 # When a user can run sshuttle as root (with or without a password),
@@ -16,27 +24,18 @@ def build_config(user_name):
 # sshuttle without needing to enter a sudo password. To use this
 # configuration, run 'visudo /etc/sudoers.d/sshuttle_auto' as root and
 # paste this text into the editor that it opens. If you want to give
-# multiple users these privileges, you may wish to use use different
+# multiple users these privileges, you may wish to use different
 # filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john).
 
 # This configuration was initially generated by the
 # 'sshuttle --sudoers-no-modify' command.
 
-Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
-
-%(user_name)s ALL=NOPASSWD: %(ca)s
-'''
+Cmnd_Alias {cmd_alias} = /usr/bin/env PYTHONPATH={dist_packages} {executable} *
 
-    content = template % {
-        # randomize command alias to avoid collisions
-        'ca': 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()},
-        'dist_packages': os.path.dirname(os.path.abspath(__file__))[:-9],
-        'py': sys.executable,
-        'path': sys.argv[0],
-        'user_name': user_name,
-    }
+{user_name} ALL=NOPASSWD: {cmd_alias}
+"""
 
-    return content
+    return template
 
 
 def sudoers(user_name=None):

From 5ce4e8c40952aa42e8611fbf0526597be690a94d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Apr 2025 10:59:08 +0000
Subject: [PATCH 275/275] build(deps): bump astral-sh/setup-uv from 5 to 6

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 .github/workflows/pythonpackage.yml  | 2 +-
 .github/workflows/release-please.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 4d0130ad9..e11c79dac 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -25,7 +25,7 @@ jobs:
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install uv
-      uses: astral-sh/setup-uv@v5
+      uses: astral-sh/setup-uv@v6
       with:
         version: "0.4.30"
         enable-cache: true
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index d259f7a5a..7b4f1d2e7 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -34,7 +34,7 @@ jobs:
       with:
         python-version: 3.12
     - name: Install uv
-      uses: astral-sh/setup-uv@v5
+      uses: astral-sh/setup-uv@v6
       with:
         version: "0.4.30"
         enable-cache: true